mirror of
https://github.com/selfhosters-cc/container-census.git
synced 2025-12-21 14:09:46 -06:00
- Always show "Manage Plugins" link in sidebar even when all plugins disabled - Restore NPM plugin static page to avoid bundle.js 404 errors - Remove npm from dynamic route generateStaticParams (uses static route) - NPM plugin now properly uses its dedicated React component - Graph and security plugins continue to use dynamic [pluginId] route This fixes the issue where disabling all plugins made it impossible to re-enable them, and resolves bundle.js loading errors for NPM plugin.
1 line
44 KiB
JavaScript
1 line
44 KiB
JavaScript
(()=>{"use strict";var n={56:(n,e,t)=>{n.exports=function(n){var e=t.nc;e&&n.setAttribute("nonce",e)}},72:n=>{var e=[];function t(n){for(var t=-1,a=0;a<e.length;a++)if(e[a].identifier===n){t=a;break}return t}function a(n,a){for(var r={},o=[],l=0;l<n.length;l++){var s=n[l],d=a.base?s[0]+a.base:s[0],c=r[d]||0,u="".concat(d," ").concat(c);r[d]=c+1;var p=t(u),b={css:s[1],media:s[2],sourceMap:s[3],supports:s[4],layer:s[5]};if(-1!==p)e[p].references++,e[p].updater(b);else{var m=i(b,a);a.byIndex=l,e.splice(l,0,{identifier:u,updater:m,references:1})}o.push(u)}return o}function i(n,e){var t=e.domAPI(e);return t.update(n),function(e){if(e){if(e.css===n.css&&e.media===n.media&&e.sourceMap===n.sourceMap&&e.supports===n.supports&&e.layer===n.layer)return;t.update(n=e)}else t.remove()}}n.exports=function(n,i){var r=a(n=n||[],i=i||{});return function(n){n=n||[];for(var o=0;o<r.length;o++){var l=t(r[o]);e[l].references--}for(var s=a(n,i),d=0;d<r.length;d++){var c=t(r[d]);0===e[c].references&&(e[c].updater(),e.splice(c,1))}r=s}}},113:n=>{n.exports=function(n,e){if(e.styleSheet)e.styleSheet.cssText=n;else{for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(document.createTextNode(n))}}},314:n=>{n.exports=function(n){var e=[];return e.toString=function(){return this.map(function(e){var t="",a=void 0!==e[5];return e[4]&&(t+="@supports (".concat(e[4],") {")),e[2]&&(t+="@media ".concat(e[2]," {")),a&&(t+="@layer".concat(e[5].length>0?" ".concat(e[5]):""," {")),t+=n(e),a&&(t+="}"),e[2]&&(t+="}"),e[4]&&(t+="}"),t}).join("")},e.i=function(n,t,a,i,r){"string"==typeof n&&(n=[[null,n,void 0]]);var o={};if(a)for(var l=0;l<this.length;l++){var s=this[l][0];null!=s&&(o[s]=!0)}for(var d=0;d<n.length;d++){var c=[].concat(n[d]);a&&o[c[0]]||(void 0!==r&&(void 0===c[5]||(c[1]="@layer".concat(c[5].length>0?" ".concat(c[5]):""," {").concat(c[1],"}")),c[5]=r),t&&(c[2]?(c[1]="@media ".concat(c[2]," {").concat(c[1],"}"),c[2]=t):c[2]=t),i&&(c[4]?(c[1]="@supports (".concat(c[4],") {").concat(c[1],"}"),c[4]=i):c[4]="".concat(i)),e.push(c))}},e}},365:(n,e,t)=>{t.d(e,{A:()=>l});var a=t(601),i=t.n(a),r=t(314),o=t.n(r)()(i());o.push([n.id,'/* Security Plugin Styles */\n\n/* Vulnerability Badge on Container Cards */\n.vulnerability-badge {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 12px;\n border-radius: 20px;\n font-size: 0.85rem;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s ease;\n border: 2px solid transparent;\n margin-left: 8px;\n}\n\n.vulnerability-badge:hover {\n transform: translateY(-2px);\n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);\n}\n\n.vulnerability-badge .vuln-icon {\n font-size: 1rem;\n}\n\n.vulnerability-badge .vuln-count {\n font-weight: 700;\n}\n\n.vulnerability-badge .vuln-severity {\n font-size: 0.75rem;\n opacity: 0.9;\n}\n\n/* Severity-based badge colors */\n.vulnerability-badge.critical {\n background: linear-gradient(135deg, #ff1744 0%, #d50000 100%);\n color: white;\n border-color: #b71c1c;\n}\n\n.vulnerability-badge.critical:hover {\n box-shadow: 0 4px 12px rgba(255, 23, 68, 0.4);\n}\n\n.vulnerability-badge.high {\n background: linear-gradient(135deg, #ff6d00 0%, #f57c00 100%);\n color: white;\n border-color: #e65100;\n}\n\n.vulnerability-badge.high:hover {\n box-shadow: 0 4px 12px rgba(255, 109, 0, 0.4);\n}\n\n.vulnerability-badge.medium {\n background: linear-gradient(135deg, #ffc107 0%, #ff9800 100%);\n color: #333;\n border-color: #f57c00;\n}\n\n.vulnerability-badge.medium:hover {\n box-shadow: 0 4px 12px rgba(255, 193, 7, 0.4);\n}\n\n.vulnerability-badge.low {\n background: linear-gradient(135deg, #8bc34a 0%, #689f38 100%);\n color: white;\n border-color: #558b2f;\n}\n\n.vulnerability-badge.clean {\n background: linear-gradient(135deg, #4caf50 0%, #388e3c 100%);\n color: white;\n border-color: #2e7d32;\n}\n\n.vulnerability-badge.not-scanned {\n background: #e0e0e0;\n color: #666;\n border-color: #bdbdbd;\n}\n\n.vulnerability-badge.remote {\n background: linear-gradient(135deg, #9c27b0 0%, #7b1fa2 100%);\n color: white;\n border-color: #6a1b9a;\n}\n\n.vulnerability-badge.scanning {\n background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);\n color: white;\n border-color: #1565c0;\n animation: pulse 1.5s ease-in-out infinite;\n}\n\n@keyframes pulse {\n 0%, 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.7;\n }\n}\n\n/* Security Tab Styles */\n.security-actions {\n display: flex;\n gap: 12px;\n margin-bottom: 24px;\n padding-bottom: 12px;\n}\n\n/* Summary Cards */\n.summary-cards {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n gap: 16px;\n margin-bottom: 24px;\n}\n\n.summary-card {\n background: white;\n padding: 20px;\n border-radius: 8px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n text-align: center;\n transition: all 0.3s ease;\n}\n\n.summary-card:hover {\n transform: translateY(-4px);\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);\n}\n\n.summary-card-title {\n font-size: 0.85rem;\n color: #666;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n margin-bottom: 8px;\n}\n\n.summary-card-value {\n font-size: 2.5rem;\n font-weight: 700;\n color: #333;\n}\n\n.summary-card.critical .summary-card-value {\n color: #d50000;\n}\n\n.summary-card.high .summary-card-value {\n color: #ff6d00;\n}\n\n.summary-card.medium .summary-card-value {\n color: #ffc107;\n}\n\n.summary-card.low .summary-card-value {\n color: #4caf50;\n}\n\n/* Queue Status Banner */\n.queue-status-banner {\n background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);\n color: white;\n padding: 16px 20px;\n border-radius: 8px;\n margin-bottom: 24px;\n font-size: 0.95rem;\n box-shadow: 0 2px 8px rgba(33, 150, 243, 0.3);\n}\n\n.queue-status-banner strong {\n font-weight: 700;\n}\n\n/* Chart Container */\n.vulnerability-chart-container {\n background: white;\n padding: 24px;\n border-radius: 8px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n margin-bottom: 24px;\n max-width: 600px;\n margin-left: auto;\n margin-right: auto;\n}\n\n/* Vulnerability Table */\n.vulnerability-table-container {\n background: white;\n border-radius: 10px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n overflow: hidden;\n}\n\n.vulnerability-table {\n width: 100%;\n border-collapse: collapse;\n}\n\n.vulnerability-table thead {\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n color: white;\n}\n\n.vulnerability-table th {\n padding: 15px;\n text-align: left;\n font-weight: 600;\n font-size: 0.9rem;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.vulnerability-table td {\n padding: 15px;\n border-bottom: 1px solid #e0e0e0;\n}\n\n.vulnerability-table td:last-child {\n white-space: nowrap;\n min-width: 200px;\n}\n\n.vulnerability-table tbody tr:hover {\n background-color: #f5f5f5;\n cursor: pointer;\n}\n\n.vulnerability-table tbody tr:last-child td {\n border-bottom: none;\n}\n\n.severity-badge {\n display: inline-block;\n padding: 4px 12px;\n border-radius: 12px;\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n margin-right: 4px;\n}\n\n.severity-badge.critical {\n background: #ffebee;\n color: #c62828;\n border: 1px solid #ef5350;\n}\n\n.severity-badge.high {\n background: #fff3e0;\n color: #e65100;\n border: 1px solid #ff9800;\n}\n\n.severity-badge.medium {\n background: #fff9c4;\n color: #f57f17;\n border: 1px solid #fbc02d;\n}\n\n.severity-badge.low {\n background: #e8f5e9;\n color: #2e7d32;\n border: 1px solid #66bb6a;\n}\n\n/* Vulnerability Details Modal Content */\n.modal-content.vulnerability-modal {\n max-width: 1400px !important;\n width: 95%;\n}\n\n.scan-metadata {\n background: #f8f9fa;\n padding: 20px;\n border-radius: 8px;\n margin-bottom: 24px;\n}\n\n.scan-metadata p {\n margin: 8px 0;\n font-size: 0.95rem;\n}\n\n.vulnerabilities-list h4 {\n margin-bottom: 16px;\n color: #333;\n}\n\n.vulnerability-filter {\n display: flex;\n gap: 12px;\n margin-bottom: 16px;\n}\n\n.vulnerability-filter input,\n.vulnerability-filter select {\n padding: 8px 12px;\n border: 1px solid #ddd;\n border-radius: 4px;\n font-size: 0.9rem;\n}\n\n.vulnerability-filter input {\n flex: 1;\n}\n\n.vuln-description {\n max-width: 400px;\n word-wrap: break-word;\n white-space: normal !important;\n line-height: 1.4;\n}\n\n/* More specific selector to ensure description wrapping in modal table */\n.modal-content .vulnerability-table .vuln-description {\n max-width: 400px;\n word-wrap: break-word;\n white-space: normal !important;\n line-height: 1.4;\n}\n\n.vuln-table {\n width: 100%;\n border-collapse: collapse;\n}\n\n.vuln-table thead {\n background: #f5f5f5;\n}\n\n.vuln-table th,\n.vuln-table td {\n padding: 12px;\n text-align: left;\n border-bottom: 1px solid #e0e0e0;\n}\n\n.vuln-table th {\n font-weight: 600;\n color: #666;\n font-size: 0.9rem;\n}\n\n.vuln-table td code {\n background: #f5f5f5;\n padding: 2px 6px;\n border-radius: 3px;\n font-size: 0.85rem;\n}\n\n.vuln-table a {\n color: #1976d2;\n text-decoration: none;\n}\n\n.vuln-table a:hover {\n text-decoration: underline;\n}\n\n/* Settings Modal */\n.settings-modal {\n max-width: 700px;\n}\n\n.settings-group {\n margin-bottom: 24px;\n padding-bottom: 20px;\n border-bottom: 1px solid #e0e0e0;\n}\n\n.settings-group:last-child {\n border-bottom: none;\n}\n\n.settings-group h4 {\n font-size: 1.1rem;\n color: #333;\n margin-bottom: 16px;\n padding-bottom: 8px;\n border-bottom: 2px solid #667eea;\n}\n\n.settings-group label {\n display: block;\n margin-bottom: 12px;\n font-size: 0.95rem;\n}\n\n.settings-group input[type="checkbox"] {\n margin-right: 8px;\n}\n\n.settings-group input[type="number"],\n.settings-group input[type="text"] {\n display: block;\n width: 100%;\n max-width: 300px;\n padding: 8px 12px;\n margin-top: 4px;\n border: 1px solid #ddd;\n border-radius: 4px;\n font-size: 0.9rem;\n}\n\n.settings-group input[disabled] {\n background: #f5f5f5;\n color: #999;\n cursor: not-allowed;\n}\n\n.settings-note {\n font-size: 0.85rem;\n color: #666;\n font-style: italic;\n margin-top: 8px;\n}\n\n/* Loading States */\n.loading-spinner {\n display: inline-block;\n width: 20px;\n height: 20px;\n border: 3px solid rgba(0, 0, 0, 0.1);\n border-radius: 50%;\n border-top-color: #667eea;\n animation: spin 0.8s linear infinite;\n}\n\n@keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n/* Empty States */\n.no-data {\n text-align: center;\n padding: 40px 20px;\n color: #666;\n font-size: 0.95rem;\n}\n\n.empty-state {\n text-align: center;\n padding: 60px 20px;\n color: #666;\n}\n\n.empty-state .empty-icon {\n font-size: 4rem;\n margin-bottom: 20px;\n opacity: 0.5;\n}\n\n.empty-state .empty-message {\n font-size: 1.2rem;\n margin-bottom: 10px;\n}\n\n.empty-state .empty-description {\n font-size: 0.9rem;\n color: #999;\n}\n/* Modal Overlay */\n.modal-overlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.6);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 10000;\n backdrop-filter: blur(2px);\n}\n\n.modal-content {\n background: var(--bg-secondary);\n border-radius: 12px;\n width: 90%;\n max-width: 900px;\n max-height: 90vh;\n overflow: hidden;\n box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);\n display: flex;\n flex-direction: column;\n border: 1px solid var(--border);\n}\n\n.modal-header {\n padding: 20px 25px;\n border-bottom: 1px solid var(--border);\n display: flex;\n justify-content: space-between;\n align-items: center;\n background: var(--bg-tertiary);\n}\n\n.modal-header h2,\n.modal-header h3 {\n margin: 0;\n color: var(--text-primary);\n max-width: calc(100% - 50px);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.modal-close {\n background: none;\n border: none;\n font-size: 32px;\n color: var(--text-secondary);\n cursor: pointer;\n padding: 0;\n width: 36px;\n height: 36px;\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 6px;\n transition: all 0.2s ease;\n line-height: 1;\n}\n\n.modal-close:hover {\n background: var(--bg-primary);\n color: var(--text-primary);\n}\n\n.modal-body {\n padding: 25px;\n overflow-y: auto;\n flex: 1;\n color: var(--text-primary);\n}\n\n.modal-footer {\n padding: 15px 25px;\n border-top: 1px solid var(--border);\n display: flex;\n gap: 10px;\n justify-content: flex-end;\n background: var(--bg-tertiary);\n}\n\n/* Dark theme adjustments for modal content */\n.modal-content .scan-metadata {\n background: var(--bg-primary);\n color: var(--text-primary);\n}\n\n.modal-content .vulnerability-table thead {\n background: var(--bg-tertiary);\n}\n\n.modal-content .vulnerability-table th {\n color: var(--text-secondary);\n}\n\n.modal-content .vulnerability-table td {\n border-color: var(--border);\n color: var(--text-primary);\n}\n\n.modal-content .vulnerability-table tbody tr:hover {\n background: var(--bg-tertiary);\n}\n\n.modal-content .vulnerability-filter input,\n.modal-content .vulnerability-filter select {\n background: var(--bg-primary);\n border-color: var(--border);\n color: var(--text-primary);\n}\n',""]);const l=o},540:n=>{n.exports=function(n){var e=document.createElement("style");return n.setAttributes(e,n.attributes),n.insert(e,n.options),e}},601:n=>{n.exports=function(n){return n[1]}},659:n=>{var e={};n.exports=function(n,t){var a=function(n){if(void 0===e[n]){var t=document.querySelector(n);if(window.HTMLIFrameElement&&t instanceof window.HTMLIFrameElement)try{t=t.contentDocument.head}catch(n){t=null}e[n]=t}return e[n]}(n);if(!a)throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");a.appendChild(t)}},825:n=>{n.exports=function(n){if("undefined"==typeof document)return{update:function(){},remove:function(){}};var e=n.insertStyleElement(n);return{update:function(t){!function(n,e,t){var a="";t.supports&&(a+="@supports (".concat(t.supports,") {")),t.media&&(a+="@media ".concat(t.media," {"));var i=void 0!==t.layer;i&&(a+="@layer".concat(t.layer.length>0?" ".concat(t.layer):""," {")),a+=t.css,i&&(a+="}"),t.media&&(a+="}"),t.supports&&(a+="}");var r=t.sourceMap;r&&"undefined"!=typeof btoa&&(a+="\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(r))))," */")),e.styleTagTransform(a,n,e.options)}(e,n,t)},remove:function(){!function(n){if(null===n.parentNode)return!1;n.parentNode.removeChild(n)}(e)}}}}},e={};function t(a){var i=e[a];if(void 0!==i)return i.exports;var r=e[a]={id:a,exports:{}};return n[a](r,r.exports,t),r.exports}t.n=n=>{var e=n&&n.__esModule?()=>n.default:()=>n;return t.d(e,{a:e}),e},t.d=(n,e)=>{for(var a in e)t.o(e,a)&&!t.o(n,a)&&Object.defineProperty(n,a,{enumerable:!0,get:e[a]})},t.o=(n,e)=>Object.prototype.hasOwnProperty.call(n,e),t.nc=void 0;var a=t(72),i=t.n(a),r=t(825),o=t.n(r),l=t(659),s=t.n(l),d=t(56),c=t.n(d),u=t(540),p=t.n(u),b=t(113),m=t.n(b),g=t(365),v={};v.styleTagTransform=m(),v.setAttributes=c(),v.insert=s().bind(null,"head"),v.domAPI=o(),v.insertStyleElement=p(),i()(g.A,v),g.A&&g.A.locals&&g.A.locals;let y={},h={},f=null,x=null,w=null;async function _(){try{const n=await fetch("/api/p/security/scans?limit=1000");if(n.ok){const e=await n.json();return h={},e.forEach(n=>{h[n.image_id]={scan:n,vulnerabilities:n.vulnerabilities||[]}}),h}}catch(n){console.error("Error preloading vulnerability scans:",n)}return{}}async function S(){try{const n=await fetch("/api/p/security/summary");if(!n.ok){if(404===n.status)return void(document.getElementById("securityTab").innerHTML='<div class="no-data">Vulnerability scanning not available</div>');throw new Error(`HTTP error! status: ${n.status}`)}const e=await n.json();!function(n,e){const{total_images_scanned:t,images_with_vulnerabilities:a,severity_counts:i}=n,r=i||{},o=document.getElementById("totalScannedImages"),l=document.getElementById("totalCriticalVulns"),s=document.getElementById("totalHighVulns"),d=document.getElementById("atRiskImages");o&&(o.textContent=t||0),l&&(l.textContent=r.critical||0),s&&(s.textContent=r.high||0),d&&(d.textContent=a||0);const c=document.getElementById("securityQueueStatus"),u=document.getElementById("queueStatusText");c&&u&&(e&&(e.in_progress>0||e.pending>0)?(c.style.display="flex",u.textContent=`${e.in_progress} active, ${e.pending} pending (${e.completed} completed, ${e.failed} failed)`):c.style.display="none"),function(n){const e=document.getElementById("vulnerabilitySeverityChart");if(!e)return;const t=e.getContext("2d");f&&f.destroy();const a={labels:["Critical","High","Medium","Low"],datasets:[{data:[n.critical||0,n.high||0,n.medium||0,n.low||0],backgroundColor:["rgba(255, 82, 82, 0.8)","rgba(255, 171, 0, 0.8)","rgba(255, 235, 59, 0.8)","rgba(76, 175, 80, 0.8)"],borderColor:["rgba(255, 82, 82, 1)","rgba(255, 171, 0, 1)","rgba(255, 235, 59, 1)","rgba(76, 175, 80, 1)"],borderWidth:1}]};f=new Chart(t,{type:"doughnut",data:a,options:{responsive:!0,maintainAspectRatio:!0,plugins:{legend:{display:!0,position:"bottom"},title:{display:!0,text:"Vulnerability Severity Distribution"}}}})}(r)}(e.summary,e.queue_status),await async function(){try{const n=await fetch("/api/p/security/scans?limit=1000");if(!n.ok)throw new Error(`HTTP error! status: ${n.status}`);const e=await n.json();!function(n){const e=document.getElementById("securityScansBody");if(!e)return;const t=n?n.filter(n=>!(!1===n.success&&n.error&&n.error.includes("image not available"))):[],a=n?n.length-t.length:0,i=[],r=new Set,o=[...t].sort((n,e)=>{const t=new Date(n.scanned_at);return new Date(e.scanned_at)-t});for(const n of o)r.has(n.image_id)||(r.add(n.image_id),i.push(n));const l=document.getElementById("scanCountBadge");l&&(l.textContent=`${i.length} images`);let s=document.getElementById("exclusionNotice");const d=document.querySelector(".security-table-card");a>0?(s||(s=document.createElement("div"),s.id="exclusionNotice",s.className="security-queue-status",s.style.marginBottom="20px",d.insertBefore(s,d.firstChild)),s.innerHTML=`\n <div class="queue-status-icon">ℹ️</div>\n <div class="queue-status-content">\n <strong>Remote Agent Images:</strong>\n ${a} images from remote agents are not shown. Agents do not have the ability to scan for vulnerabilities.\n </div>\n `,s.style.display="flex"):s&&(s.style.display="none"),i&&0!==i.length?e.innerHTML=i.map(n=>{const e=n.severity_counts||{},t=n.total_vulnerabilities||0,a=new Date(n.scanned_at).toLocaleString();let i="";return i=n.success?0===t?"✅ Clean":e.critical>0?"🚨 Critical":e.high>0?"⚠️ High":"📊 Issues":"❌ Failed",`\n <tr data-image-id="${n.image_id}" style="cursor: pointer;" onclick="window.viewScanDetails('${n.image_id}')">\n <td title="${n.image_name||n.image_id}">${n.image_name||n.image_id}</td>\n <td>${i}</td>\n <td>${t}</td>\n <td>${e.critical||0}</td>\n <td>${e.high||0}</td>\n <td>${e.medium||0}</td>\n <td>${e.low||0}</td>\n <td>${a}</td>\n <td>\n <button class="btn btn-secondary" onclick="event.stopPropagation(); window.viewScanDetails('${n.image_id}')">View</button>\n <button class="btn btn-primary" onclick="event.stopPropagation(); rescanImage('${n.image_id}', '${n.image_name||n.image_id}')">Rescan</button>\n </td>\n </tr>\n `}).join(""):e.innerHTML='<tr><td colspan="9" class="no-data">No vulnerability scans found</td></tr>'}(e),function(n){const e=document.getElementById("vulnerabilityTrendsChart");if(e)try{if(!n||0===n.length)return void console.log("No scan data available for trends chart");const t=new Date,a=new Date(t.getTime()-2592e6),i={};n.forEach(n=>{if(!n.success||!n.scanned_at)return;const e=new Date(n.scanned_at);if(e<a)return;const t=e.toISOString().split("T")[0];i[t]||(i[t]={critical:0,high:0,medium:0,low:0,total:0,count:0});const r=n.severity_counts||{};i[t].critical+=r.critical||0,i[t].high+=r.high||0,i[t].medium+=r.medium||0,i[t].low+=r.low||0,i[t].total+=n.total_vulnerabilities||0,i[t].count++});const r=Object.keys(i).sort(),o=r.map(n=>new Date(n).toLocaleDateString("en-US",{month:"short",day:"numeric"})),l=r.map(n=>i[n].critical),s=r.map(n=>i[n].high),d=r.map(n=>i[n].medium),c=r.map(n=>i[n].low);x&&x.destroy(),x=new Chart(e,{type:"line",data:{labels:o,datasets:[{label:"Critical",data:l,borderColor:"#ff1744",backgroundColor:"rgba(255, 23, 68, 0.1)",borderWidth:2,fill:!0,tension:.4},{label:"High",data:s,borderColor:"#ff9800",backgroundColor:"rgba(255, 152, 0, 0.1)",borderWidth:2,fill:!0,tension:.4},{label:"Medium",data:d,borderColor:"#ffc107",backgroundColor:"rgba(255, 193, 7, 0.1)",borderWidth:2,fill:!0,tension:.4},{label:"Low",data:c,borderColor:"#4caf50",backgroundColor:"rgba(76, 175, 80, 0.1)",borderWidth:2,fill:!0,tension:.4}]},options:{responsive:!0,maintainAspectRatio:!1,interaction:{mode:"index",intersect:!1},plugins:{legend:{position:"bottom",labels:{color:"var(--text-secondary)",font:{size:13},padding:12,usePointStyle:!0}},title:{display:!1},tooltip:{backgroundColor:"rgba(0, 0, 0, 0.8)",padding:12,titleFont:{size:14,weight:"bold"},bodyFont:{size:13},callbacks:{footer:function(n){let e=0;return n.forEach(n=>{e+=n.parsed.y}),"Total: "+e}}}},scales:{x:{grid:{display:!1},ticks:{color:"var(--text-tertiary)",font:{size:11}}},y:{beginAtZero:!0,grid:{color:"rgba(100, 100, 100, 0.1)"},ticks:{color:"var(--text-tertiary)",font:{size:11},precision:0}}}}})}catch(n){console.error("Error rendering trends chart:",n)}}(e)}catch(n){console.error("Error loading vulnerability scans:",n)}}()}catch(n){console.error("Error loading vulnerability summary:",n),document.getElementById("securityTab").innerHTML='<div class="no-data">Error loading vulnerability data</div>'}}window.viewVulnerabilityDetails=function(n){window.switchTab("security"),setTimeout(()=>{const e=document.getElementById("securitySearch");e&&(e.value=n,e.dispatchEvent(new Event("input")))},100)},window.viewScanDetails=async function(n){try{const e=await fetch(`/api/p/security/image/${n}`);if(!e.ok)throw new Error(`HTTP error! status: ${e.status}`);!function(n){const{scan:e,vulnerabilities:t}=n,a=document.createElement("div");a.className="modal-overlay",a.innerHTML=`\n <div class="modal-content vulnerability-modal">\n <div class="modal-header">\n <h3>Vulnerability Scan: ${e.image_name}</h3>\n <button onclick="this.closest('.modal-overlay').remove()" class="modal-close">×</button>\n </div>\n <div class="modal-body">\n <div class="scan-metadata">\n <p><strong>Image ID:</strong> ${e.image_id}</p>\n <p><strong>Scanned:</strong> ${new Date(e.scanned_at).toLocaleString()}</p>\n <p><strong>Total Vulnerabilities:</strong> ${e.total_vulnerabilities}</p>\n <p><strong>Severity Breakdown:</strong>\n ${e.severity_counts.critical||0} Critical,\n ${e.severity_counts.high||0} High,\n ${e.severity_counts.medium||0} Medium,\n ${e.severity_counts.low||0} Low\n </p>\n </div>\n\n <div class="vulnerabilities-list">\n <h4>Vulnerabilities</h4>\n ${t&&t.length>0?`\n <div class="vulnerability-filter">\n <input type="text" id="vulnSearch" placeholder="Filter by CVE, package, or description..."\n oninput="filterVulnerabilities(this.value)">\n <select id="vulnSeverityFilter" onchange="filterVulnerabilities()">\n <option value="">All Severities</option>\n <option value="CRITICAL">Critical</option>\n <option value="HIGH">High</option>\n <option value="MEDIUM">Medium</option>\n <option value="LOW">Low</option>\n </select>\n </div>\n <table class="vulnerability-table" id="vulnerabilityDetailsTable">\n <thead>\n <tr>\n <th>CVE</th>\n <th>Package</th>\n <th>Severity</th>\n <th>Installed</th>\n <th>Fixed</th>\n <th>Description</th>\n </tr>\n </thead>\n <tbody>\n ${t.map(n=>`\n <tr data-severity="${n.severity}" data-cve="${n.vulnerability_id}" data-pkg="${n.pkg_name}" data-desc="${n.description||""}">\n <td><a href="https://nvd.nist.gov/vuln/detail/${n.vulnerability_id}" target="_blank">${n.vulnerability_id}</a></td>\n <td>${n.pkg_name}</td>\n <td><span class="severity-badge ${n.severity.toLowerCase()}">${n.severity}</span></td>\n <td>${n.installed_version}</td>\n <td>${n.fixed_version||"N/A"}</td>\n <td class="vuln-description">${n.description||n.title||"No description"}</td>\n </tr>\n `).join("")}\n </tbody>\n </table>\n `:'<p class="no-data">No vulnerabilities found</p>'}\n </div>\n </div>\n <div class="modal-footer">\n <button onclick="rescanImage('${e.image_id}')" class="btn">Rescan</button>\n <button onclick="this.closest('.modal-overlay').remove()" class="btn btn-secondary">Close</button>\n </div>\n </div>\n `,document.body.appendChild(a)}(await e.json())}catch(n){console.error("Error loading scan details:",n),alert("Error loading vulnerability details")}},window.filterVulnerabilities=function(){const n=document.getElementById("vulnSearch")?.value.toLowerCase()||"",e=document.getElementById("vulnSeverityFilter")?.value||"";document.querySelectorAll("#vulnerabilityDetailsTable tbody tr").forEach(t=>{const a=t.getAttribute("data-cve").toLowerCase(),i=t.getAttribute("data-pkg").toLowerCase(),r=t.getAttribute("data-desc").toLowerCase(),o=t.getAttribute("data-severity"),l=!n||a.includes(n)||i.includes(n)||r.includes(n),s=!e||o===e;t.style.display=l&&s?"":"none"})},window.rescanImage=async function(n){try{const e=await fetch(`/api/p/security/scan/${n}`,{method:"POST"});if(!e.ok)throw new Error(`HTTP error! status: ${e.status}`);alert("Scan queued successfully. Results will be available in about 30 seconds."),document.querySelector(".modal-overlay")?.remove(),setTimeout(()=>{S()},2e3)}catch(n){console.error("Error triggering scan:",n),alert("Error queueing scan")}},window.scanAllImages=async function(){if(confirm("This will queue all known images for rescanning. Continue?"))try{const n=await fetch("/api/p/security/scan-all",{method:"POST"});if(!n.ok)throw new Error(`HTTP error! status: ${n.status}`);const e=await n.json();alert(`${e.images_queued} images queued for scanning`),setTimeout(()=>{S()},2e3)}catch(n){console.error("Error triggering scan all:",n),alert("Error queueing scans")}},window.updateTrivyDB=async function(){if(!confirm("This will update the Trivy vulnerability database. This may take a few minutes. Continue?"))return;const n=event.target;n.disabled=!0,n.textContent="Updating...";try{const n=await fetch("/api/p/security/update-db",{method:"POST"});if(!n.ok)throw new Error(`HTTP error! status: ${n.status}`);alert("Trivy database updated successfully")}catch(n){console.error("Error updating Trivy DB:",n),alert("Error updating Trivy database")}finally{n.disabled=!1,n.textContent="Update DB"}},window.exportVulnerabilityData=function(){const n=Array.from(document.querySelectorAll("#securityScansTable tbody tr")).filter(n=>!n.querySelector(".no-data")).map(n=>{const e=n.querySelectorAll("td");return{image:e[0].textContent,status:e[1].textContent,total:e[2].textContent,breakdown:e[3].textContent,scanned_at:e[4].textContent}}),e=[["Image","Status","Total Vulnerabilities","Severity Breakdown","Scanned At"].join(","),...n.map(n=>[n.image,n.status,n.total,n.breakdown,n.scanned_at].join(","))].join("\n"),t=new Blob([e],{type:"text/csv"}),a=URL.createObjectURL(t),i=document.createElement("a");i.href=a,i.download="vulnerability-scans.csv",i.click()},window.showSecuritySettings=async function(){try{const n=await fetch("/api/p/security/settings");if(!n.ok)throw new Error(`HTTP error! status: ${n.status}`);const e=await n.json(),t=document.createElement("div");t.className="modal-overlay",t.innerHTML=`\n <div class="modal-content settings-modal">\n <div class="modal-header">\n <h3>Security Scanner Settings</h3>\n <button onclick="this.closest('.modal-overlay').remove()" class="modal-close">×</button>\n </div>\n <div class="modal-body">\n <form id="securitySettingsForm">\n <div class="settings-group">\n <h4>General</h4>\n <label>\n <input type="checkbox" id="enabled" ${e.enabled?"checked":""}>\n Enable vulnerability scanning\n </label>\n <label>\n <input type="checkbox" id="auto_scan_new_images" ${e.auto_scan_new_images?"checked":""}>\n Auto-scan new images\n </label>\n </div>\n\n <div class="settings-group">\n <h4>Performance</h4>\n <label>\n Worker Pool Size (1-10):\n <input type="number" id="worker_pool_size" value="${e.worker_pool_size}" min="1" max="10">\n </label>\n <label>\n Scan Timeout (minutes):\n <input type="number" id="scan_timeout_minutes" value="${e.scan_timeout_minutes}" min="1" max="60">\n </label>\n <label>\n Max Queue Size:\n <input type="number" id="max_queue_size" value="${e.max_queue_size}" min="10" max="1000">\n </label>\n </div>\n\n <div class="settings-group">\n <h4>Cache & Rescanning</h4>\n <label>\n Cache TTL (hours):\n <input type="number" id="cache_ttl_hours" value="${e.cache_ttl_hours}" min="1" max="168">\n </label>\n <label>\n Rescan Interval (hours):\n <input type="number" id="rescan_interval_hours" value="${e.rescan_interval_hours}" min="1" max="720">\n </label>\n </div>\n\n <div class="settings-group">\n <h4>Retention</h4>\n <label>\n Retention Days (metadata):\n <input type="number" id="retention_days" value="${e.retention_days}" min="1" max="365">\n </label>\n <label>\n Detailed Retention Days (CVEs):\n <input type="number" id="detailed_retention_days" value="${e.detailed_retention_days}" min="1" max="90">\n </label>\n </div>\n\n <div class="settings-group">\n <h4>Notifications</h4>\n <label>\n <input type="checkbox" id="alert_on_critical" ${e.alert_on_critical?"checked":""}>\n Alert on Critical vulnerabilities\n </label>\n <label>\n <input type="checkbox" id="alert_on_high" ${e.alert_on_high?"checked":""}>\n Alert on High vulnerabilities\n </label>\n </div>\n\n <div class="settings-group">\n <h4>Storage</h4>\n <label>\n Cache Directory (read-only):\n <input type="text" value="${e.cache_dir}" readonly disabled>\n </label>\n <p class="settings-note">Database Update Interval: ${e.db_update_interval_hours} hours</p>\n </div>\n </form>\n </div>\n <div class="modal-footer">\n <button onclick="saveSecuritySettings()" class="btn">Save Settings</button>\n <button onclick="this.closest('.modal-overlay').remove()" class="btn btn-secondary">Cancel</button>\n </div>\n </div>\n `,document.body.appendChild(t)}catch(n){console.error("Error loading settings:",n),alert("Error loading settings")}},window.saveSecuritySettings=async function(){const n=document.getElementById("securitySettingsForm"),e={enabled:n.querySelector("#enabled").checked,auto_scan_new_images:n.querySelector("#auto_scan_new_images").checked,worker_pool_size:parseInt(n.querySelector("#worker_pool_size").value),scan_timeout_minutes:parseInt(n.querySelector("#scan_timeout_minutes").value),max_queue_size:parseInt(n.querySelector("#max_queue_size").value),cache_ttl_hours:parseInt(n.querySelector("#cache_ttl_hours").value),rescan_interval_hours:parseInt(n.querySelector("#rescan_interval_hours").value),retention_days:parseInt(n.querySelector("#retention_days").value),detailed_retention_days:parseInt(n.querySelector("#detailed_retention_days").value),alert_on_critical:n.querySelector("#alert_on_critical").checked,alert_on_high:n.querySelector("#alert_on_high").checked};try{const n=await fetch("/api/p/security/settings",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!n.ok)throw new Error(`HTTP error! status: ${n.status}`);alert("Settings saved successfully"),document.querySelector(".modal-overlay")?.remove()}catch(n){console.error("Error saving settings:",n),alert("Error saving settings")}},window.filterSecurityScans=function(){const n=document.getElementById("securitySearch")?.value.toLowerCase()||"";document.querySelectorAll("#securityScansTable tbody tr").forEach(e=>{if(e.querySelector(".no-data"))return;const t=e.getAttribute("data-image-id"),a=e.querySelector("td:first-child").textContent.toLowerCase(),i=!n||a.includes(n)||t.includes(n);e.style.display=i?"":"none"})},window.initSecurityPlugin=function(n,e){console.log("[SecurityPlugin] Initializing with container:",n),n?(n.innerHTML='\n <div id="securityTab" class="security-section-modern">\n <div class="security-header-modern">\n <div class="security-title-group">\n <h2>🛡️ Vulnerability Scanner</h2>\n <p class="security-subtitle">Monitor and track security vulnerabilities across all container images</p>\n </div>\n <div class="security-actions">\n <button id="scanAllImagesBtn" class="btn btn-primary">\n 🔄 Scan All Images\n </button>\n <button id="updateTrivyDBBtn" class="btn btn-secondary">\n 📥 Update Database\n </button>\n <button id="vulnerabilitySettingsBtn" class="btn btn-secondary">\n ⚙️ Settings\n </button>\n </div>\n </div>\n\n <div class="security-summary-grid">\n <div class="security-summary-card-modern">\n <div class="card-header">\n <span class="card-icon">📊</span>\n <span class="card-label">Total Scanned</span>\n </div>\n <div class="card-value" id="totalScannedImages">-</div>\n <div class="card-footer">images analyzed</div>\n </div>\n <div class="security-summary-card-modern critical-card">\n <div class="card-header">\n <span class="card-icon">🚨</span>\n <span class="card-label">Critical</span>\n </div>\n <div class="card-value" id="totalCriticalVulns">-</div>\n <div class="card-footer">vulnerabilities</div>\n </div>\n <div class="security-summary-card-modern high-card">\n <div class="card-header">\n <span class="card-icon">⚠️</span>\n <span class="card-label">High</span>\n </div>\n <div class="card-value" id="totalHighVulns">-</div>\n <div class="card-footer">vulnerabilities</div>\n </div>\n <div class="security-summary-card-modern">\n <div class="card-header">\n <span class="card-icon">🛡️</span>\n <span class="card-label">At Risk</span>\n </div>\n <div class="card-value" id="atRiskImages">-</div>\n <div class="card-footer">images affected</div>\n </div>\n </div>\n\n <div class="security-queue-status" id="securityQueueStatus" style="display: none;">\n <div class="queue-status-icon">⏳</div>\n <div class="queue-status-content">\n <strong>Scanning in progress:</strong>\n <span id="queueStatusText">-</span>\n </div>\n </div>\n\n <div class="security-charts-grid">\n <div class="security-chart-card">\n <div class="chart-card-header">\n <h3>Severity Distribution</h3>\n <p class="chart-subtitle">Current vulnerability breakdown</p>\n </div>\n <div class="security-chart-container">\n <canvas id="vulnerabilitySeverityChart"></canvas>\n </div>\n </div>\n\n <div class="security-chart-card">\n <div class="chart-card-header">\n <h3>Vulnerability Trends</h3>\n <p class="chart-subtitle">Last 30 days</p>\n </div>\n <div class="security-chart-container">\n <canvas id="vulnerabilityTrendsChart"></canvas>\n </div>\n </div>\n </div>\n\n <div class="security-table-card">\n <div class="security-table-header-modern">\n <div class="table-title-group">\n <h3>Vulnerability Scans</h3>\n <span class="scan-count" id="scanCountBadge">0 scans</span>\n </div>\n <div class="security-filters-modern">\n <select id="securitySeverityFilter" class="filter-select">\n <option value="">All Severities</option>\n <option value="critical">Critical</option>\n <option value="high">High</option>\n <option value="medium">Medium</option>\n <option value="low">Low</option>\n <option value="clean">Clean</option>\n </select>\n <select id="securityStatusFilter" class="filter-select">\n <option value="">All Status</option>\n <option value="scanned">Scanned Only</option>\n <option value="remote">Remote Only</option>\n <option value="failed">Failed Only</option>\n </select>\n <input type="text" id="securitySearchInput" class="search-input" placeholder="🔍 Search images...">\n </div>\n </div>\n <div class="table-container">\n <table class="security-table-modern">\n <thead>\n <tr>\n <th>Image Name</th>\n <th>Status</th>\n <th>Total</th>\n <th>Critical</th>\n <th>High</th>\n <th>Medium</th>\n <th>Low</th>\n <th>Scanned</th>\n <th>Actions</th>\n </tr>\n </thead>\n <tbody id="securityScansBody">\n <tr>\n <td colspan="9" class="loading">Loading...</td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n </div>\n ',async function(){await _(),await S(),new MutationObserver(()=>{const n=document.getElementById("securityTab");n&&!n.classList.contains("hidden")?w||(w=setInterval(S,3e4)):w&&(clearInterval(w),w=null)}).observe(document.getElementById("securityTab"),{attributes:!0,attributeFilter:["class"]})}()):console.error("[SecurityPlugin] No container element provided")},window.getVulnerabilityBadgeHTML=function(n){if(!n)return'<span class="vulnerability-badge not-scanned" title="Not scanned">🛡️ Not Scanned</span>';if(!n.scan.success){const e=n.scan.error||"";return e.includes("image not available for scanning")||e.includes("not available")?'<span class="vulnerability-badge remote" title="Remote image - not available for scanning">🌐 Remote</span>':'<span class="vulnerability-badge not-scanned" title="Scan failed">⚠️ Scan Failed</span>'}const e=n.scan.severity_counts||{},t=n.scan.total_vulnerabilities||0,a=e.critical||0,i=e.high||0,r=e.medium||0,o=e.low||0;if(0===t)return'<span class="vulnerability-badge clean" title="No vulnerabilities found">✓ Clean</span>';let l="low",s="🛡️";a>0?(l="critical",s="🚨"):i>0?(l="high",s="⚠️"):r>0&&(l="medium",s="⚡");let d=`${s} ${t}`;(a>0||i>0)&&(d+=` (${a}C ${i}H)`);const c=[];return a>0&&c.push(`${a} Critical`),i>0&&c.push(`${i} High`),r>0&&c.push(`${r} Medium`),o>0&&c.push(`${o} Low`),`<span class="vulnerability-badge ${l}" title="Total: ${t} vulnerabilities - ${c.join(", ")}" onclick="window.viewVulnerabilityDetails('${n.scan.image_id}')">${d}</span>`},window.getVulnerabilityScan=async function(n){return y[n]?y[n]:h&&h[n]?(y[n]=h[n],h[n]):(y[n]=null,null)},window.preloadVulnerabilityScans=_})(); |