Files
container-census/internal/plugins/builtin/security/frontend/bundle.js
Self Hosters 8f960fbf68 Fix sidebar navigation and plugin routing issues
- 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.
2025-12-07 20:22:52 -05:00

1 line
44 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(()=>{"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=_})();