Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"/> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>SafeAIScan Pro</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link href="https://fonts.googleapis.com/css2?family=Syne:wght@600;700;800&family=DM+Sans:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"/> | |
| <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet"/> | |
| <link rel="stylesheet" href="styles.css"> | |
| <style> | |
| :root { --sg:#00ffa3; --kr:#ff4d6d; } | |
| /* ββ Layout ββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .pro-layout { | |
| display: grid; | |
| grid-template-columns: 240px 1fr; | |
| grid-template-rows: 54px 1fr; | |
| min-height: 100vh; | |
| } | |
| /* ββ Topbar ββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .pro-topbar { | |
| grid-column: 1 / -1; | |
| background: var(--bg-1); | |
| border-bottom: 1px solid var(--border); | |
| display: flex; align-items: center; | |
| justify-content: space-between; | |
| padding: 0 20px; gap: 12px; | |
| position: sticky; top: 0; z-index: 200; | |
| } | |
| .pro-brand { | |
| font-family: 'Syne', sans-serif; | |
| font-size: 15px; font-weight: 800; | |
| letter-spacing: -0.03em; | |
| display: flex; align-items: center; gap: 8px; | |
| text-decoration: none; color: var(--text-primary); | |
| } | |
| .pro-badge-tag { | |
| font-size: 9px; font-weight: 700; | |
| text-transform: uppercase; letter-spacing: 1.2px; | |
| padding: 3px 8px; border-radius: 99px; | |
| background: linear-gradient(135deg,rgba(91,123,254,.25),rgba(0,255,163,.2)); | |
| color: var(--sg); border: 1px solid rgba(0,255,163,.3); | |
| } | |
| .topbar-center { | |
| flex: 1; display: flex; align-items: center; | |
| justify-content: center; gap: 8px; | |
| } | |
| .scan-mode-btn { | |
| display: flex; align-items: center; gap: 6px; | |
| background: var(--bg-2); border: 1px solid var(--border); | |
| color: var(--text-muted); border-radius: 8px; | |
| padding: 7px 14px; font-size: 12px; font-weight: 500; | |
| cursor: pointer; transition: all .2s; white-space: nowrap; | |
| } | |
| .scan-mode-btn.active, .scan-mode-btn:hover { | |
| border-color: var(--accent); | |
| background: rgba(91,123,254,.1); | |
| color: #93aaff; | |
| } | |
| .scan-mode-btn.active { font-weight: 600; } | |
| .topbar-right { | |
| display: flex; align-items: center; gap: 10px; | |
| } | |
| .usage-pill { | |
| font-size: 11px; color: var(--text-muted); | |
| background: var(--bg-2); border: 1px solid var(--border); | |
| border-radius: 99px; padding: 4px 12px; | |
| display: flex; align-items: center; gap: 5px; | |
| } | |
| .usage-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--sg); } | |
| /* ββ Sidebar ββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .pro-sidebar { | |
| background: var(--bg-1); | |
| border-right: 1px solid var(--border); | |
| padding: 16px 12px; | |
| overflow-y: auto; | |
| display: flex; flex-direction: column; gap: 4px; | |
| } | |
| .side-section-label { | |
| font-size: 10px; font-weight: 700; | |
| text-transform: uppercase; letter-spacing: 1px; | |
| color: var(--text-faint); padding: 10px 8px 4px; | |
| } | |
| .side-nav-item { | |
| display: flex; align-items: center; gap: 10px; | |
| padding: 9px 10px; border-radius: 10px; | |
| font-size: 13px; color: var(--text-muted); | |
| cursor: pointer; transition: all .15s; | |
| text-decoration: none; border: none; background: none; | |
| width: 100%; text-align: left; | |
| } | |
| .side-nav-item:hover { background: var(--bg-2); color: var(--text-primary); } | |
| .side-nav-item.active { | |
| background: rgba(91,123,254,.12); | |
| color: #93aaff; font-weight: 600; | |
| border: 1px solid rgba(91,123,254,.2); | |
| } | |
| .side-nav-item i { font-size: 15px; flex-shrink: 0; } | |
| .pro-feature-badge { | |
| margin-left: auto; font-size: 8px; font-weight: 700; | |
| text-transform: uppercase; letter-spacing: .5px; | |
| padding: 2px 6px; border-radius: 4px; | |
| background: rgba(0,255,163,.1); color: var(--sg); | |
| border: 1px solid rgba(0,255,163,.2); | |
| } | |
| /* ββ Main content βββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .pro-main { | |
| background: var(--bg-base); | |
| overflow-y: auto; | |
| display: flex; flex-direction: column; | |
| } | |
| /* ββ Panels βββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .pro-panel { display: none; flex: 1; } | |
| .pro-panel.active { display: flex; flex-direction: column; } | |
| /* ββ Code scanner panel βββββββββββββββββββββββββββββββββββββββββ */ | |
| .scanner-area { | |
| display: grid; | |
| grid-template-columns: 1fr 340px; | |
| gap: 0; | |
| flex: 1; | |
| overflow: hidden; | |
| } | |
| .scanner-left { | |
| display: flex; flex-direction: column; | |
| border-right: 1px solid var(--border); | |
| overflow: hidden; | |
| } | |
| .editor-header { | |
| display: flex; align-items: center; gap: 10px; | |
| padding: 10px 16px; | |
| border-bottom: 1px solid var(--border); | |
| background: var(--bg-1); | |
| flex-shrink: 0; | |
| } | |
| .lang-select { | |
| font-size: 12px; background: var(--bg-2); | |
| border: 1px solid var(--border); color: var(--text-muted); | |
| border-radius: 6px; padding: 4px 8px; cursor: pointer; | |
| } | |
| .editor-actions { display: flex; gap: 6px; margin-left: auto; } | |
| .btn-sm-action { | |
| display: flex; align-items: center; gap: 5px; | |
| font-size: 12px; font-weight: 500; | |
| padding: 6px 12px; border-radius: 8px; | |
| border: none; cursor: pointer; transition: all .15s; | |
| } | |
| .btn-scan-primary { | |
| background: linear-gradient(135deg, #5b7bfe, #4361ee); | |
| color: #fff; | |
| box-shadow: 0 4px 14px rgba(91,123,254,.35); | |
| } | |
| .btn-scan-primary:hover { box-shadow: 0 6px 20px rgba(91,123,254,.5); } | |
| .btn-scan-secondary { | |
| background: var(--bg-2); color: var(--text-muted); | |
| border: 1px solid var(--border); | |
| } | |
| .btn-scan-secondary:hover { border-color: var(--accent); color: #93aaff; } | |
| .btn-scan-success { | |
| background: rgba(0,255,163,.1); color: var(--sg); | |
| border: 1px solid rgba(0,255,163,.25); | |
| } | |
| .btn-scan-success:hover { background: rgba(0,255,163,.18); } | |
| textarea.code-editor { | |
| flex: 1; width: 100%; resize: none; | |
| background: #040c18; | |
| color: #e8edf8; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 13px; line-height: 1.65; | |
| border: none; outline: none; | |
| padding: 16px 20px; | |
| min-height: 0; | |
| } | |
| textarea.code-editor::placeholder { color: #2a3a55; } | |
| /* ββ Scan progress bar ββββββββββββββββββββββββββββββββββββββββββ */ | |
| .scan-progress-bar { | |
| height: 2px; width: 0%; | |
| background: linear-gradient(90deg, #5b7bfe, #00ffa3); | |
| transition: width .4s ease; | |
| flex-shrink: 0; | |
| } | |
| /* ββ Results sidebar ββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .scanner-right { | |
| background: var(--bg-1); | |
| display: flex; flex-direction: column; | |
| overflow: hidden; | |
| } | |
| .results-header { | |
| padding: 12px 16px; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; align-items: center; gap: 8px; | |
| flex-shrink: 0; | |
| } | |
| .results-title { | |
| font-size: 12px; font-weight: 700; | |
| text-transform: uppercase; letter-spacing: .8px; | |
| color: var(--text-muted); | |
| } | |
| .risk-badge { | |
| margin-left: auto; font-size: 10px; font-weight: 700; | |
| padding: 3px 10px; border-radius: 99px; | |
| } | |
| .risk-none { background: rgba(52,211,153,.12); color: #34d399; } | |
| .risk-low { background: rgba(250,204,21,.1); color: #fbbf24; } | |
| .risk-medium { background: rgba(251,146,60,.12); color: #fb923c; } | |
| .risk-high { background: rgba(244,63,94,.12); color: #f43f5e; } | |
| .risk-critical { background: rgba(192,38,211,.14); color: #c026d3; } | |
| .results-list { | |
| flex: 1; overflow-y: auto; | |
| padding: 8px; | |
| } | |
| .finding-card { | |
| background: var(--bg-2); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; padding: 12px; | |
| margin-bottom: 8px; cursor: pointer; | |
| transition: border-color .15s, box-shadow .15s; | |
| animation: fadeSlide .25s ease both; | |
| } | |
| .finding-card:hover { | |
| border-color: var(--border-bright); | |
| box-shadow: 0 4px 16px rgba(0,0,0,.3); | |
| } | |
| @keyframes fadeSlide { | |
| from { opacity: 0; transform: translateX(8px); } | |
| to { opacity: 1; transform: translateX(0); } | |
| } | |
| .finding-type { | |
| font-size: 12px; font-weight: 600; | |
| color: var(--text-primary); margin-bottom: 4px; | |
| } | |
| .finding-meta { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; color: #38bdf8; | |
| margin-bottom: 4px; | |
| } | |
| .finding-match { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; color: var(--text-faint); | |
| background: rgba(4,12,24,.8); | |
| padding: 3px 6px; border-radius: 4px; | |
| word-break: break-all; | |
| } | |
| .sev-chip { | |
| display: inline-block; font-size: 9px; font-weight: 700; | |
| text-transform: uppercase; letter-spacing: .5px; | |
| padding: 2px 7px; border-radius: 4px; margin-bottom: 6px; | |
| } | |
| .sev-CRITICAL { background: rgba(192,38,211,.16); color: #e879f9; } | |
| .sev-HIGH { background: rgba(244,63,94,.15); color: #fb7185; } | |
| .sev-MEDIUM { background: rgba(251,146,60,.12); color: #fdba74; } | |
| .sev-LOW { background: rgba(250,204,21,.1); color: #fbbf24; } | |
| /* PHASE 1: badge-pill β used for compliance (OWASP/NIST) and auto-fix badges */ | |
| .badge-pill { | |
| display: inline-flex; align-items: center; gap: 4px; | |
| font-size: 10px; font-weight: 600; | |
| padding: 2px 8px; border-radius: 99px; | |
| line-height: 1.6; | |
| } | |
| .results-empty { | |
| padding: 32px 16px; text-align: center; | |
| color: var(--text-faint); font-size: 13px; | |
| } | |
| .results-summary { | |
| padding: 10px 16px; | |
| border-top: 1px solid var(--border); | |
| background: var(--bg-2); | |
| display: flex; gap: 12px; flex-shrink: 0; | |
| } | |
| .sum-stat { | |
| text-align: center; flex: 1; | |
| } | |
| .sum-num { | |
| font-family: 'Syne', sans-serif; | |
| font-size: 20px; font-weight: 800; line-height: 1; | |
| } | |
| .sum-lbl { font-size: 9px; text-transform: uppercase; color: var(--text-faint); margin-top: 2px; } | |
| /* ββ Repo scan panel ββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .repo-panel-inner { | |
| max-width: 700px; margin: 40px auto; | |
| padding: 0 24px; width: 100%; | |
| } | |
| .panel-card { | |
| background: var(--bg-2); | |
| border: 1px solid var(--border); | |
| border-radius: 18px; padding: 28px; | |
| } | |
| .panel-card-title { | |
| font-family: 'Syne', sans-serif; | |
| font-size: 18px; font-weight: 800; | |
| margin-bottom: 6px; display: flex; align-items: center; gap: 10px; | |
| } | |
| .panel-card-sub { font-size: 13px; color: var(--text-muted); margin-bottom: 22px; } | |
| .url-input-wrap { position: relative; } | |
| .url-input-wrap i { | |
| position: absolute; left: 12px; top: 50%; | |
| transform: translateY(-50%); | |
| color: var(--text-faint); font-size: 14px; pointer-events: none; | |
| } | |
| .url-input-wrap input { padding-left: 36px ; } | |
| /* Task progress */ | |
| .task-progress { | |
| display: none; margin-top: 16px; | |
| padding: 16px; background: var(--bg-1); | |
| border: 1px solid var(--border); border-radius: 12px; | |
| } | |
| .task-progress.show { display: block; } | |
| .task-state-label { | |
| font-size: 12px; font-weight: 600; | |
| color: var(--text-primary); margin-bottom: 8px; | |
| display: flex; align-items: center; gap: 6px; | |
| } | |
| .progress-track { | |
| height: 6px; background: var(--bg-3); | |
| border-radius: 99px; overflow: hidden; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, #5b7bfe, #00ffa3); | |
| border-radius: 99px; | |
| transition: width .5s ease; | |
| } | |
| /* ββ ZIP panel ββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .zip-drop-zone { | |
| border: 2px dashed var(--border-bright); | |
| border-radius: 16px; padding: 48px; | |
| text-align: center; cursor: pointer; | |
| transition: all .2s; | |
| background: var(--bg-1); | |
| } | |
| .zip-drop-zone:hover, .zip-drop-zone.drag-over { | |
| border-color: var(--sg); | |
| background: rgba(0,255,163,.04); | |
| } | |
| .zip-icon { | |
| font-size: 48px; color: var(--text-faint); | |
| margin-bottom: 12px; display: block; | |
| } | |
| .zip-title { font-size: 16px; font-weight: 600; margin-bottom: 6px; } | |
| .zip-sub { font-size: 13px; color: var(--text-muted); } | |
| input[type=file]#zipInput { display: none; } | |
| /* ββ CVE lookup panel βββββββββββββββββββββββββββββββββββββββββββ */ | |
| .cve-result-card { | |
| background: var(--bg-2); border: 1px solid var(--border); | |
| border-radius: 10px; padding: 14px; margin-bottom: 10px; | |
| animation: fadeSlide .2s ease both; | |
| } | |
| .cve-id { font-size: 13px; font-weight: 700; color: #38bdf8; } | |
| .cve-score-badge { | |
| font-size: 10px; font-weight: 700; | |
| padding: 2px 8px; border-radius: 99px; float: right; | |
| } | |
| .cve-desc { font-size: 12px; color: var(--text-muted); margin-top: 6px; line-height: 1.5; } | |
| /* ββ AI Analysis panel ββββββββββββββββββββββββββββββββββββββββββ */ | |
| .ai-panel-inner { | |
| max-width: 800px; margin: 0 auto; padding: 24px; | |
| width: 100%; | |
| } | |
| .ai-input-section { | |
| background: var(--bg-2); border: 1px solid var(--border); | |
| border-radius: 14px; overflow: hidden; margin-bottom: 20px; | |
| } | |
| .ai-result-section { | |
| background: var(--bg-2); border: 1px solid var(--border); | |
| border-radius: 14px; padding: 20px; | |
| } | |
| .ai-explanation { | |
| font-size: 14px; color: var(--text-muted); | |
| line-height: 1.7; margin-bottom: 16px; | |
| } | |
| .ai-fix-item { | |
| display: flex; gap: 10px; align-items: flex-start; | |
| margin-bottom: 10px; padding: 10px 12px; | |
| background: rgba(91,123,254,.06); | |
| border-left: 3px solid var(--accent); | |
| border-radius: 0 8px 8px 0; | |
| } | |
| .fix-num { | |
| background: var(--accent); color: #fff; | |
| font-size: 10px; font-weight: 700; | |
| width: 18px; height: 18px; border-radius: 50%; | |
| display: flex; align-items: center; justify-content: center; | |
| flex-shrink: 0; margin-top: 1px; | |
| } | |
| .fix-text { font-size: 13px; color: var(--text-muted); line-height: 1.6; } | |
| /* ββ PDF / Report panel βββββββββββββββββββββββββββββββββββββββββ */ | |
| .reports-grid { | |
| display: grid; gap: 12px; | |
| grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); | |
| padding: 24px; | |
| } | |
| .report-card { | |
| background: var(--bg-2); border: 1px solid var(--border); | |
| border-radius: 14px; padding: 18px; | |
| display: flex; flex-direction: column; gap: 10px; | |
| transition: border-color .2s; | |
| } | |
| .report-card:hover { border-color: var(--border-bright); } | |
| .report-meta { font-size: 11px; color: var(--text-faint); } | |
| .report-risk { font-size: 12px; font-weight: 700; } | |
| .report-source { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; color: #38bdf8; | |
| white-space: nowrap; overflow: hidden; text-overflow: ellipsis; | |
| } | |
| /* ββ Log strip ββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .log-strip { | |
| padding: 4px 16px; | |
| background: #040c18; | |
| border-top: 1px solid rgba(99,130,255,.08); | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; color: var(--text-faint); | |
| flex-shrink: 0; | |
| overflow: hidden; white-space: nowrap; text-overflow: ellipsis; | |
| } | |
| .log-ok { color: var(--sg); } | |
| .log-warn { color: var(--warning); } | |
| .log-err { color: var(--danger); } | |
| /* ββ Toast ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .toast-area { | |
| position: fixed; bottom: 20px; right: 20px; | |
| z-index: 9999; display: flex; | |
| flex-direction: column; gap: 8px; align-items: flex-end; | |
| } | |
| .toast-item { | |
| padding: 10px 16px; border-radius: 10px; | |
| font-size: 13px; font-family: 'DM Sans', sans-serif; | |
| font-weight: 500; backdrop-filter: blur(12px); | |
| box-shadow: 0 8px 32px rgba(0,0,0,.4); | |
| display: flex; align-items: center; gap: 8px; | |
| animation: toastIn .25s ease both; | |
| cursor: pointer; max-width: 340px; | |
| } | |
| @keyframes toastIn { | |
| from { opacity: 0; transform: translateY(12px) scale(.95); } | |
| to { opacity: 1; transform: translateY(0) scale(1); } | |
| } | |
| .toast-success { background: rgba(52,211,153,.12); color: #34d399; border: 1px solid rgba(52,211,153,.25); } | |
| .toast-error { background: rgba(244,63,94,.12); color: #f43f5e; border: 1px solid rgba(244,63,94,.25); } | |
| .toast-info { background: rgba(91,123,254,.12); color: #93aaff; border: 1px solid rgba(91,123,254,.25); } | |
| .toast-warning { background: rgba(251,146,60,.12); color: #fdba74; border: 1px solid rgba(251,146,60,.25); } | |
| /* ββ Detail drawer ββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .detail-drawer { | |
| position: fixed; right: 0; top: 54px; bottom: 0; | |
| width: 420px; background: var(--bg-1); | |
| border-left: 1px solid var(--border-bright); | |
| z-index: 300; transform: translateX(100%); | |
| transition: transform .3s cubic-bezier(.4,0,.2,1); | |
| overflow-y: auto; padding: 20px; | |
| display: flex; flex-direction: column; gap: 14px; | |
| } | |
| .detail-drawer.open { transform: translateX(0); } | |
| .detail-drawer { touch-action: pan-y; } | |
| .drawer-close { | |
| position: absolute; top: 12px; right: 14px; | |
| background: var(--bg-3); border: 1px solid var(--border); | |
| color: var(--text-muted); border-radius: 8px; | |
| width: 28px; height: 28px; cursor: pointer; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 16px; line-height: 1; | |
| } | |
| .drawer-field-label { | |
| font-size: 10px; text-transform: uppercase; | |
| letter-spacing: .8px; color: var(--text-faint); | |
| margin-bottom: 4px; font-weight: 600; | |
| } | |
| .drawer-field-value { | |
| font-size: 13px; color: var(--text-muted); | |
| background: var(--bg-2); border: 1px solid var(--border); | |
| border-radius: 8px; padding: 10px 12px; line-height: 1.6; | |
| } | |
| .drawer-fix-box { | |
| background: rgba(91,123,254,.08); | |
| border: 1px solid rgba(91,123,254,.2); | |
| border-radius: 8px; padding: 12px; | |
| font-size: 13px; color: var(--text-muted); line-height: 1.6; | |
| } | |
| /* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| MOBILE & TABLET OPTIMIZATION β "On the go coders" | |
| Tiers: β₯1025px desktop (unchanged) Β· 769-1024px tablet Β· | |
| 481-768px phone-landscape/small-tablet Β· β€480px phone | |
| All additive β desktop layout above is untouched. | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| /* ββ Mobile menu button (hidden on desktop) βββββββββββββββββββ */ | |
| .mobile-menu-btn { | |
| display: none; | |
| background: var(--bg-2); | |
| border: 1px solid var(--border); | |
| color: var(--text-muted); | |
| border-radius: 8px; | |
| width: 36px; height: 36px; | |
| align-items: center; justify-content: center; | |
| font-size: 18px; cursor: pointer; | |
| flex-shrink: 0; | |
| } | |
| .mobile-sidebar-close { display: none; } | |
| .mobile-sidebar-overlay { | |
| display: none; | |
| position: fixed; inset: 0; | |
| background: rgba(4,12,24,.6); | |
| backdrop-filter: blur(2px); | |
| z-index: 250; | |
| opacity: 0; | |
| transition: opacity .25s ease; | |
| } | |
| .mobile-sidebar-overlay.show { | |
| display: block; | |
| opacity: 1; | |
| } | |
| body.mobile-nav-open { overflow: hidden; } | |
| /* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| TABLET TIER β 769px to 1024px | |
| Narrower sidebar, narrower results column, larger touch targets | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| @media (max-width: 1024px) { | |
| .pro-layout { grid-template-columns: 76px 1fr; } | |
| .pro-sidebar { padding: 12px 6px; } | |
| .side-section-label { display: none; } | |
| .side-nav-item { | |
| justify-content: center; | |
| padding: 12px 6px; | |
| flex-direction: column; | |
| gap: 4px; | |
| font-size: 9px; | |
| text-align: center; | |
| } | |
| .side-nav-item i { font-size: 18px; } | |
| .side-nav-item span:not(.pro-feature-badge) { display: none; } | |
| .pro-feature-badge { position: absolute; top: 4px; right: 4px; } | |
| .side-nav-item { position: relative; } | |
| #sidebarTrialCTA, #sidebarEmail, #sidebarEmail + a, | |
| div[style*="Signed in as"] { display: none ; } | |
| .scanner-area { grid-template-columns: 1fr 300px; } | |
| .topbar-center { gap: 4px; } | |
| .scan-mode-btn { | |
| padding: 7px 10px; | |
| font-size: 11px; | |
| } | |
| .scan-mode-btn i { font-size: 13px; } | |
| .detail-drawer { width: 360px; } | |
| .ai-panel-inner, .repo-panel-inner { padding: 0 20px; } | |
| } | |
| /* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PHONE / SMALL TABLET TIER β β€768px | |
| Sidebar becomes slide-out drawer, topbar mode tabs scroll | |
| horizontally, scanner stacks vertically, drawer goes full-screen | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| @media (max-width: 768px) { | |
| html, body { overflow-x: hidden; } | |
| /* ββ Layout: single column, sidebar off-canvas ββ */ | |
| .pro-layout { | |
| grid-template-columns: 1fr; | |
| grid-template-rows: auto 1fr; | |
| } | |
| /* ββ Topbar ββ */ | |
| .pro-topbar { | |
| padding: 0 10px; | |
| gap: 8px; | |
| height: 52px; | |
| flex-wrap: nowrap; | |
| } | |
| .mobile-menu-btn { display: flex; } | |
| .pro-brand { font-size: 13px; gap: 6px; } | |
| .pro-brand span:first-of-type { display: none; } /* hide "SafeAIScan" text wordmark, keep icon+badge */ | |
| .pro-brand .pro-badge-tag { display: none; } | |
| /* Topbar mode tabs: horizontal scroll instead of center-flex/wrap */ | |
| .topbar-center { | |
| position: absolute; | |
| left: 0; right: 0; top: 52px; | |
| flex: none; | |
| justify-content: flex-start; | |
| overflow-x: auto; | |
| overflow-y: hidden; | |
| -webkit-overflow-scrolling: touch; | |
| scroll-snap-type: x proximity; | |
| gap: 6px; | |
| padding: 8px 10px; | |
| background: var(--bg-1); | |
| border-bottom: 1px solid var(--border); | |
| scrollbar-width: none; | |
| } | |
| .topbar-center::-webkit-scrollbar { display: none; } | |
| .scan-mode-btn { | |
| flex-shrink: 0; | |
| scroll-snap-align: start; | |
| padding: 8px 12px; | |
| font-size: 12px; | |
| min-height: 40px; | |
| } | |
| /* Push main content down to clear the now-wrapped topbar+tabs */ | |
| .pro-layout { grid-template-rows: 96px 1fr; } | |
| .topbar-right { gap: 6px; } | |
| .usage-pill { font-size: 10px; padding: 4px 8px; } | |
| .usage-pill span:not(.usage-dot) { max-width: 80px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | |
| .hide-on-mobile { display: none ; } | |
| /* ββ Sidebar becomes a slide-out drawer ββ */ | |
| .pro-sidebar { | |
| position: fixed; | |
| top: 0; bottom: 0; left: 0; | |
| width: 78vw; | |
| max-width: 300px; | |
| z-index: 260; | |
| transform: translateX(-100%); | |
| transition: transform .25s ease; | |
| box-shadow: 8px 0 32px rgba(0,0,0,.5); | |
| padding-top: 56px; | |
| } | |
| .pro-sidebar.mobile-open { transform: translateX(0); } | |
| .mobile-sidebar-close { | |
| display: flex; | |
| position: absolute; top: 10px; right: 10px; | |
| background: var(--bg-3); border: 1px solid var(--border); | |
| color: var(--text-muted); border-radius: 8px; | |
| width: 32px; height: 32px; | |
| align-items: center; justify-content: center; | |
| font-size: 14px; cursor: pointer; | |
| } | |
| .side-section-label { display: block; } | |
| .side-nav-item { | |
| flex-direction: row; | |
| justify-content: flex-start; | |
| font-size: 14px; | |
| padding: 12px 10px; | |
| min-height: 44px; | |
| } | |
| .side-nav-item i { font-size: 16px; } | |
| .side-nav-item span:not(.pro-feature-badge) { display: inline; } | |
| #sidebarTrialCTA, #sidebarEmail, div[style*="Signed in as"] { display: block ; } | |
| /* ββ Scanner area: stack editor above results ββ */ | |
| .scanner-area { | |
| grid-template-columns: 1fr; | |
| grid-template-rows: 1fr auto; | |
| overflow: visible; | |
| } | |
| .scanner-left { | |
| border-right: none; | |
| border-bottom: 1px solid var(--border); | |
| min-height: 280px; | |
| } | |
| .scanner-right { | |
| height: auto; | |
| max-height: 60vh; | |
| } | |
| textarea.code-editor { | |
| font-size: 16px; /* prevents iOS auto-zoom on focus */ | |
| padding: 12px 14px; | |
| min-height: 220px; | |
| } | |
| /* ββ Editor header: wrap actions onto their own row ββ */ | |
| .editor-header { | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| padding: 8px 12px; | |
| } | |
| .editor-actions { | |
| width: 100%; | |
| margin-left: 0; | |
| justify-content: flex-end; | |
| } | |
| .btn-sm-action { | |
| padding: 8px 12px; | |
| font-size: 12px; | |
| min-height: 40px; | |
| } | |
| .lang-select { min-height: 36px; } | |
| /* ββ Results summary: 2x2 grid instead of 1x4 row ββ */ | |
| .results-summary { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 8px; | |
| padding: 12px; | |
| } | |
| .sum-stat { | |
| background: var(--bg-1); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 8px 4px; | |
| } | |
| /* ββ Security score card ββ */ | |
| #securityScoreCard { padding: 12px; } | |
| /* ββ Repo health: 2-column grid on phones ββ */ | |
| #repoHealthCard div[style*="grid-template-columns:repeat(auto-fit"], | |
| div[style*="grid-template-columns:repeat(auto-fit,minmax(90px"] { | |
| grid-template-columns: repeat(2, 1fr) ; | |
| } | |
| /* ββ Detail drawer: full screen with swipe handle ββ */ | |
| .detail-drawer { | |
| top: 0; | |
| width: 100%; | |
| padding: 20px 16px 24px; | |
| border-left: none; | |
| } | |
| .detail-drawer::before { | |
| content: ''; | |
| display: block; | |
| width: 40px; height: 4px; | |
| background: var(--border-bright); | |
| border-radius: 99px; | |
| margin: 0 auto 12px; | |
| } | |
| .drawer-close { | |
| width: 36px; height: 36px; | |
| top: 16px; right: 16px; | |
| font-size: 18px; | |
| } | |
| /* AI auto-fix before/after: stack into single column */ | |
| .vuln-details > div[style*="grid-template-columns:1fr 1fr"], | |
| div[style*="display:grid;grid-template-columns:1fr 1fr;gap:8px"] { | |
| grid-template-columns: 1fr ; | |
| } | |
| /* ββ Repo / ZIP / AI / CVE panels: full-width with safe padding ββ */ | |
| .repo-panel-inner, .ai-panel-inner { | |
| max-width: 100%; | |
| margin: 16px auto; | |
| padding: 0 14px; | |
| } | |
| .panel-card { padding: 18px; border-radius: 14px; } | |
| .zip-drop-zone { padding: 28px 16px; } | |
| .zip-icon { font-size: 36px; } | |
| /* ββ Reports grid: single column ββ */ | |
| .reports-grid { | |
| grid-template-columns: 1fr; | |
| padding: 14px; | |
| } | |
| /* ββ CVE / dependency tables: horizontal scroll wrapper ββ */ | |
| .dep-table { display: block; overflow-x: auto; white-space: nowrap; } | |
| /* ββ Log strip: allow wrap, smaller font ββ */ | |
| .log-strip { font-size: 10px; padding: 4px 12px; } | |
| /* ββ Toasts: full width, anchored to bottom ββ */ | |
| .toast-area { | |
| left: 12px; right: 12px; bottom: 12px; | |
| align-items: stretch; | |
| } | |
| .toast-item { max-width: none; width: 100%; } | |
| /* ββ Touch target floor for any interactive element ββ */ | |
| button, .btn, a.btn, select, input[type="text"], input[type="url"], | |
| input[type="email"], input[type="password"], input[type="file"] + label { | |
| min-height: 40px; | |
| } | |
| } | |
| /* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SMALL PHONE TIER β β€480px | |
| Tighter spacing, single-column everything, smaller type scale | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| @media (max-width: 480px) { | |
| .pro-brand { font-size: 12px; } | |
| .pro-layout { grid-template-rows: 90px 1fr; } | |
| .topbar-center { top: 48px; padding: 6px 8px; } | |
| .pro-topbar { height: 48px; } | |
| .scan-mode-btn { | |
| padding: 7px 10px; | |
| font-size: 11px; | |
| } | |
| .scan-mode-btn span, .scan-mode-btn { white-space: nowrap; } | |
| .usage-pill span:not(.usage-dot) { max-width: 56px; } | |
| textarea.code-editor { font-size: 15px; padding: 10px 12px; } | |
| .results-summary { grid-template-columns: 1fr 1fr; gap: 6px; padding: 8px; } | |
| .sum-num { font-size: 17px; } | |
| #securityScoreCard svg { width: 48px; height: 48px; } | |
| #repoHealthCard div[style*="grid-template-columns:repeat(auto-fit"], | |
| div[style*="grid-template-columns:repeat(auto-fit,minmax(90px"] { | |
| grid-template-columns: repeat(2, 1fr) ; | |
| } | |
| .panel-card { padding: 14px; border-radius: 12px; } | |
| .panel-card-title { font-size: 15px; } | |
| .results-list { padding: 6px; } | |
| .finding-card { padding: 10px; } | |
| .drawer-fix-box, .drawer-field-value { font-size: 12px; padding: 8px 10px; } | |
| .editor-actions { gap: 4px; } | |
| .btn-sm-action { padding: 8px 10px; font-size: 11px; } | |
| .btn-sm-action span { display: none; } /* icon-only buttons on tiny screens */ | |
| .btn-sm-action i { font-size: 14px; } | |
| } | |
| /* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| LANDSCAPE PHONES β short viewport height | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| @media (max-width: 900px) and (orientation: landscape) and (max-height: 500px) { | |
| .pro-layout { grid-template-rows: 48px 1fr; } | |
| .topbar-center { | |
| position: static; | |
| top: auto; | |
| flex: 1; | |
| justify-content: center; | |
| border-bottom: none; | |
| padding: 0; | |
| } | |
| .pro-topbar { height: 48px; flex-wrap: nowrap; } | |
| .scanner-left { min-height: 180px; } | |
| .scanner-right { max-height: 40vh; } | |
| textarea.code-editor { min-height: 140px; } | |
| } | |
| /* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| TOUCH DEVICES β applies regardless of screen size | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| @media (pointer: coarse) { | |
| .scan-mode-btn, .side-nav-item, .btn-sm-action, | |
| .finding-card, .drawer-close, .mobile-menu-btn, | |
| .copy-btn, .seats-btn, .method-tab { | |
| min-height: 40px; | |
| } | |
| .finding-card { padding: 14px 12px; } | |
| /* Slightly larger tap targets for chevrons / icon-only buttons */ | |
| .drawer-close, .mobile-menu-btn, .mobile-sidebar-close { | |
| width: 40px; height: 40px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Auth guard: allow pro AND active trial users; redirect only expired/free --> | |
| <script> | |
| (function() { | |
| var token = localStorage.getItem('access_token'); | |
| var isPro = localStorage.getItem('is_pro') === 'true'; | |
| var plan = localStorage.getItem('user_plan') || 'free'; | |
| var trial = localStorage.getItem('trial_active') === 'true'; | |
| if (!token || token === 'undefined') { | |
| window.location.replace('login.html'); | |
| return; | |
| } | |
| // Allow: paid pro, active trial β block only plain free | |
| var hasAccess = isPro || trial || plan === 'pro' || plan === 'pro_trial' || plan === 'enterprise'; | |
| if (!hasAccess) { | |
| window.location.replace('checkout.html?reason=pro_required'); | |
| } | |
| })(); | |
| </script> | |
| <div class="pro-layout"> | |
| <!-- ββ TOPBAR βββββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <header class="pro-topbar"> | |
| <button class="mobile-menu-btn" id="mobileMenuBtn" onclick="toggleMobileSidebar()" aria-label="Open menu"> | |
| <i class="bi bi-list"></i> | |
| </button> | |
| <a href="dashboard.html" class="pro-brand" style="text-decoration:none;"> | |
| <i class="bi bi-shield-lock-fill" style="color:var(--sg);font-size:16px;"></i> | |
| <span><span style="color:var(--accent);">Safe</span><span>AI</span><span style="color:var(--sg);">Scan</span></span> | |
| <span class="pro-badge-tag">PRO</span> | |
| </a> | |
| <div class="topbar-center"> | |
| <button class="scan-mode-btn active" id="mode-code" onclick="setMode('code')"> | |
| <i class="bi bi-code-slash"></i> Code Scan | |
| </button> | |
| <button class="scan-mode-btn" id="mode-zip" onclick="setMode('zip')"> | |
| <i class="bi bi-file-zip"></i> ZIP Upload | |
| </button> | |
| <button class="scan-mode-btn" id="mode-repo" onclick="setMode('repo')"> | |
| <i class="bi bi-github"></i> Repo Scan | |
| </button> | |
| <button class="scan-mode-btn" id="mode-ai" onclick="setMode('ai')"> | |
| <i class="bi bi-cpu"></i> AI Analysis | |
| </button> | |
| <button class="scan-mode-btn" id="mode-cve" onclick="setMode('cve')"> | |
| <i class="bi bi-bug"></i> CVE Lookup | |
| </button> | |
| <button class="scan-mode-btn" id="mode-reports" onclick="setMode('reports')"> | |
| <i class="bi bi-file-earmark-pdf"></i> Reports | |
| </button> | |
| </div> | |
| <div class="topbar-right"> | |
| <div class="usage-pill"> | |
| <span class="usage-dot"></span> | |
| <span id="usagePill">Unlimited scans</span> | |
| </div> | |
| <a href="dashboard.html" class="btn-sm-action btn-scan-secondary hide-on-mobile" style="text-decoration:none;font-size:12px;padding:6px 12px;border-radius:8px;"> | |
| <i class="bi bi-grid"></i> Dashboard | |
| </a> | |
| <button class="btn-sm-action btn-scan-secondary" onclick="doLogout()" style="padding:6px 10px;"> | |
| <i class="bi bi-box-arrow-right"></i> | |
| </button> | |
| </div> | |
| </header> | |
| <!-- ββ MOBILE SIDEBAR OVERLAY βββββββββββββββββββββββββββββββ --> | |
| <div class="mobile-sidebar-overlay" id="mobileSidebarOverlay" onclick="closeMobileSidebar()"></div> | |
| <!-- ββ SIDEBAR ββββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <aside class="pro-sidebar" id="proSidebar"> | |
| <button class="mobile-sidebar-close" id="mobileSidebarClose" onclick="closeMobileSidebar()" aria-label="Close menu"> | |
| <i class="bi bi-x-lg"></i> | |
| </button> | |
| <div class="side-section-label">Scanner</div> | |
| <button class="side-nav-item active" id="sb-code" onclick="setMode('code')"><i class="bi bi-code-slash"></i>Code Scanner</button> | |
| <button class="side-nav-item" id="sb-zip" onclick="setMode('zip')"><i class="bi bi-file-zip"></i>ZIP Upload<span class="pro-feature-badge">PRO</span></button> | |
| <button class="side-nav-item" id="sb-repo" onclick="setMode('repo')"><i class="bi bi-github"></i>Repo Scan<span class="pro-feature-badge">PRO</span></button> | |
| <div class="side-section-label">Intelligence</div> | |
| <button class="side-nav-item" id="sb-ai" onclick="setMode('ai')"><i class="bi bi-cpu"></i>Deep AI Analysis<span class="pro-feature-badge">PRO</span></button> | |
| <button class="side-nav-item" id="sb-cve" onclick="setMode('cve')"><i class="bi bi-bug"></i>CVE Lookup<span class="pro-feature-badge">PRO</span></button> | |
| <div class="side-section-label">Reports</div> | |
| <button class="side-nav-item" id="sb-reports" onclick="setMode('reports')"><i class="bi bi-file-earmark-pdf"></i>PDF Reports<span class="pro-feature-badge">PRO</span></button> | |
| <div style="margin-top:auto; padding-top:16px; border-top:1px solid var(--border);"> | |
| <!-- Trial upgrade CTA β shown only when on trial --> | |
| <div id="sidebarTrialCTA" style="display:none;margin-bottom:12px;padding:10px;background:linear-gradient(135deg,rgba(0,255,163,.08),rgba(91,123,254,.06));border:1px solid rgba(0,255,163,.2);border-radius:10px;"> | |
| <div style="font-size:10px;font-weight:700;color:#00ffa3;text-transform:uppercase;letter-spacing:.7px;margin-bottom:4px;"> | |
| <i class="bi bi-gift-fill me-1"></i>Free Trial Active | |
| </div> | |
| <div id="sidebarTrialDays" style="font-size:11px;color:var(--text-muted);margin-bottom:8px;line-height:1.5;"> | |
| Enjoying all Pro features for free. | |
| </div> | |
| <a href="checkout.html" style="display:block;text-align:center;background:linear-gradient(135deg,#5b7bfe,#4361ee);color:#fff;border-radius:8px;padding:7px;font-size:11px;font-weight:600;text-decoration:none;"> | |
| Upgrade β $1.99/mo | |
| </a> | |
| </div> | |
| <div style="font-size:11px;color:var(--text-faint);padding:8px 8px 4px;">Signed in as</div> | |
| <div id="sidebarEmail" style="font-size:12px;color:var(--text-muted);padding:0 8px 8px;word-break:break-all;"></div> | |
| <a href="index.html" class="side-nav-item"><i class="bi bi-arrow-left"></i>Back to Dashboard</a> | |
| </div> | |
| </aside> | |
| <!-- ββ MAIN βββββββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <main class="pro-main"> | |
| <!-- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PANEL 1: CODE SCANNER | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="pro-panel active" id="panel-code"> | |
| <div class="scan-progress-bar" id="progressBar"></div> | |
| <div class="scanner-area"> | |
| <div class="scanner-left"> | |
| <div class="editor-header"> | |
| <select class="lang-select" id="langSelect"> | |
| <option>Python</option><option>JavaScript</option><option>TypeScript</option> | |
| <option>Go</option><option>Java</option><option>Ruby</option> | |
| <option>PHP</option><option>Shell</option><option>YAML</option> | |
| <option>JSON</option><option>Terraform</option><option>Other</option> | |
| </select> | |
| <span style="font-size:11px;color:var(--text-faint);">Paste code to scan for secrets</span> | |
| <div class="editor-actions"> | |
| <button class="btn-sm-action btn-scan-secondary" onclick="clearCode()"> | |
| <i class="bi bi-trash"></i> Clear | |
| </button> | |
| <button class="btn-sm-action btn-scan-secondary" id="aiOnlyBtn" onclick="runAIOnly()"> | |
| <i class="bi bi-cpu"></i> AI Only | |
| </button> | |
| <button class="btn-sm-action btn-scan-primary" id="scanBtn" onclick="runCodeScan()"> | |
| <i class="bi bi-play-circle-fill"></i> Run Scan | |
| </button> | |
| </div> | |
| </div> | |
| <textarea class="code-editor" id="codeInput" | |
| placeholder="# Paste your code here... # Example of what we detect: OPENAI_API_KEY = "sk-abc123..." GITHUB_TOKEN = "ghp_xyz..." AWS_ACCESS_KEY_ID = "AKIA...""></textarea> | |
| <div class="log-strip" id="logStrip"> | |
| <span class="log-ok">β</span> SafeAIScan Pro ready β all features unlocked | |
| </div> | |
| </div> | |
| <div class="scanner-right"> | |
| <!-- PHASE 1: Security Score (weighted scoring engine) --> | |
| <div id="securityScoreCard" style="display:none;background:var(--bg-2);border:1px solid var(--border);border-radius:12px;padding:14px;margin-bottom:12px;"> | |
| <div style="display:flex;align-items:center;gap:14px;"> | |
| <div style="position:relative;width:56px;height:56px;flex-shrink:0;"> | |
| <svg width="56" height="56" viewBox="0 0 56 56" style="transform:rotate(-90deg);"> | |
| <circle cx="28" cy="28" r="24" fill="none" stroke="var(--bg-3)" stroke-width="5"/> | |
| <circle id="scoreRing" cx="28" cy="28" r="24" fill="none" stroke="#facc15" stroke-width="5" | |
| stroke-dasharray="150.8" stroke-dashoffset="150.8" stroke-linecap="round"/> | |
| </svg> | |
| <div id="scoreRingValue" style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-family:'Syne',sans-serif;font-weight:800;font-size:16px;">β</div> | |
| </div> | |
| <div> | |
| <div style="font-size:10px;text-transform:uppercase;letter-spacing:0.7px;color:var(--text-faint);">Security Score</div> | |
| <div id="scoreRiskLabel" style="font-family:'Syne',sans-serif;font-weight:800;font-size:15px;">β</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="results-header"> | |
| <span class="results-title">Findings</span> | |
| <button onclick="exportCurrentPDF()" style="background:none;border:none;color:var(--text-faint);font-size:11px;cursor:pointer;display:flex;align-items:center;gap:4px;" title="Download PDF"> | |
| <i class="bi bi-file-earmark-pdf"></i> PDF | |
| </button> | |
| <button onclick="exportExecutivePDF()" style="background:none;border:none;color:var(--text-faint);font-size:11px;cursor:pointer;display:flex;align-items:center;gap:4px;" title="Download Executive Report"> | |
| <i class="bi bi-file-earmark-richtext"></i> Executive | |
| </button> | |
| <span class="risk-badge risk-none" id="riskBadge">NONE</span> | |
| </div> | |
| <div class="results-list" id="resultsList"> | |
| <div class="results-empty"> | |
| <i class="bi bi-shield-check" style="font-size:32px;color:var(--text-faint);display:block;margin-bottom:8px;"></i> | |
| Run a scan to see findings | |
| </div> | |
| </div> | |
| <div class="results-summary" id="resultsSummary"> | |
| <div class="sum-stat"> | |
| <div class="sum-num" id="sumTotal" style="color:var(--text-primary);">β</div> | |
| <div class="sum-lbl">Total</div> | |
| </div> | |
| <div class="sum-stat"> | |
| <div class="sum-num" id="sumHigh" style="color:#fb7185;">β</div> | |
| <div class="sum-lbl">HIGH</div> | |
| </div> | |
| <div class="sum-stat"> | |
| <div class="sum-num" id="sumMed" style="color:#fdba74;">β</div> | |
| <div class="sum-lbl">MED</div> | |
| </div> | |
| <div class="sum-stat"> | |
| <div class="sum-num" id="sumLow" style="color:#fbbf24;">β</div> | |
| <div class="sum-lbl">LOW</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PANEL 2: ZIP UPLOAD | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="pro-panel" id="panel-zip"> | |
| <div class="scan-progress-bar" id="zipProgressBar"></div> | |
| <div class="repo-panel-inner"> | |
| <div class="panel-card"> | |
| <div class="panel-card-title"> | |
| <i class="bi bi-file-zip" style="color:var(--sg);"></i> | |
| ZIP File Upload | |
| </div> | |
| <div class="panel-card-sub"> | |
| Upload a ZIP archive of your project. We extract it, scan every eligible file for secrets, and return full results β nothing is stored. | |
| </div> | |
| <input type="file" id="zipInput" accept=".zip"> | |
| <div class="zip-drop-zone" id="dropZone" onclick="document.getElementById('zipInput').click()"> | |
| <i class="bi bi-file-zip zip-icon"></i> | |
| <div class="zip-title">Drop your ZIP here</div> | |
| <div class="zip-sub">or click to browse — max 50 MB, up to 1,000 files</div> | |
| </div> | |
| <div class="task-progress" id="zipProgress"> | |
| <div class="task-state-label"> | |
| <span class="spinner-border spinner-border-sm" style="border-color:var(--sg);border-right-color:transparent;"></span> | |
| <span id="zipStateLabel">Scanningβ¦</span> | |
| </div> | |
| <div class="progress-track"> | |
| <div class="progress-fill" id="zipProgressFill" style="width:0%;"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ZIP results appear here --> | |
| <div id="zipResults" style="margin-top:16px;"></div> | |
| </div> | |
| </div> | |
| <!-- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PANEL 3: REPO SCAN | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="pro-panel" id="panel-repo"> | |
| <div class="repo-panel-inner"> | |
| <div class="panel-card"> | |
| <div class="panel-card-title"> | |
| <i class="bi bi-github" style="color:var(--sg);"></i> | |
| GitHub Repository Scan | |
| </div> | |
| <div class="panel-card-sub"> | |
| Paste a public GitHub repository URL. SecretScan clones it (depth=1), scans every file, then deletes the clone. Your code is never stored. | |
| </div> | |
| <div class="url-input-wrap mb-3"> | |
| <i class="bi bi-github"></i> | |
| <input id="repoUrl" type="text" class="form-control" | |
| placeholder="https://github.com/owner/repo" | |
| value="https://github.com/"> | |
| </div> | |
| <button class="btn-sm-action btn-scan-primary w-100" style="justify-content:center;padding:12px;" | |
| onclick="runRepoScan()"> | |
| <i class="bi bi-play-circle-fill"></i> Start Repo Scan | |
| </button> | |
| <div class="task-progress" id="repoProgress" style="margin-top:16px;"> | |
| <div class="task-state-label"> | |
| <span class="spinner-border spinner-border-sm" style="border-color:var(--sg);border-right-color:transparent;"></span> | |
| <span id="repoStateLabel">Queuedβ¦</span> | |
| </div> | |
| <div class="progress-track"> | |
| <div class="progress-fill" id="repoProgressFill" style="width:0%;"></div> | |
| </div> | |
| <div id="repoProgressMsg" style="font-size:11px;color:var(--text-faint);margin-top:6px;"></div> | |
| </div> | |
| </div> | |
| <div id="repoResults" style="margin-top:16px;"></div> | |
| </div> | |
| </div> | |
| <!-- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PANEL 4: DEEP AI ANALYSIS | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="pro-panel" id="panel-ai"> | |
| <div class="ai-panel-inner"> | |
| <div style="margin-bottom:16px;"> | |
| <div style="font-family:'Syne',sans-serif;font-weight:800;font-size:18px;margin-bottom:4px;"> | |
| <i class="bi bi-cpu" style="color:var(--sg);margin-right:8px;"></i>Deep AI Analysis | |
| </div> | |
| <div style="font-size:13px;color:var(--text-muted);"> | |
| Full AI reasoning over your code β attack surface mapping, severity prediction, OWASP categorisation, and actionable fix steps. | |
| </div> | |
| </div> | |
| <div class="ai-input-section"> | |
| <textarea id="aiCodeInput" style="width:100%;background:#040c18;border:none;outline:none;padding:16px;font-family:'JetBrains Mono',monospace;font-size:13px;color:#e8edf8;min-height:200px;resize:vertical;border-radius:14px 14px 0 0;" | |
| placeholder="Paste code for deep AI security analysisβ¦"></textarea> | |
| <div style="padding:10px 14px;border-top:1px solid var(--border);display:flex;gap:8px;justify-content:flex-end;"> | |
| <button class="btn-sm-action btn-scan-primary" id="aiAnalyzeBtn" onclick="runDeepAI()"> | |
| <i class="bi bi-cpu"></i> Analyse with AI | |
| </button> | |
| </div> | |
| </div> | |
| <div class="ai-result-section" id="aiResult" style="display:none;"> | |
| <div style="font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--sg);font-weight:700;margin-bottom:12px;"> | |
| <i class="bi bi-cpu me-1"></i>AI Security Report | |
| </div> | |
| <div class="ai-explanation" id="aiExplanation"></div> | |
| <div style="font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--accent);font-weight:700;margin-bottom:8px;" id="aiFixesLabel" style="display:none;"> | |
| <i class="bi bi-wrench-adjustable me-1"></i>Recommended Fixes | |
| </div> | |
| <div id="aiFixesList"></div> | |
| <div id="aiRiskRow" style="margin-top:12px;font-size:12px;color:var(--text-muted);display:none;"> | |
| <i class="bi bi-shield-exclamation" style="color:var(--warning);"></i> | |
| Predicted severity: <strong id="aiSeverity" style="color:var(--warning);"></strong> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PANEL 5: CVE LOOKUP | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="pro-panel" id="panel-cve"> | |
| <div class="repo-panel-inner"> | |
| <div class="panel-card"> | |
| <div class="panel-card-title"> | |
| <i class="bi bi-bug" style="color:#fb7185;"></i> | |
| CVE Lookup | |
| </div> | |
| <div class="panel-card-sub"> | |
| Search the NVD CVE database by CVE ID or keyword. Results show CVSS score, severity, and a description. | |
| </div> | |
| <div style="display:flex;gap:8px;"> | |
| <input id="cveInput" type="text" class="form-control" | |
| placeholder="CVE-2024-12345 or sql injection" | |
| onkeydown="if(event.key==='Enter') runCVELookup()"> | |
| <button class="btn-sm-action btn-scan-primary" onclick="runCVELookup()" style="white-space:nowrap;flex-shrink:0;"> | |
| <i class="bi bi-search"></i> Search | |
| </button> | |
| </div> | |
| <div id="cveResults" style="margin-top:16px;"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PANEL 6: PDF REPORTS | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="pro-panel" id="panel-reports"> | |
| <div style="padding:20px 24px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;"> | |
| <div> | |
| <div style="font-family:'Syne',sans-serif;font-weight:800;font-size:18px;"> | |
| <i class="bi bi-file-earmark-pdf" style="color:#c026d3;margin-right:8px;"></i>Scan History & Reports | |
| </div> | |
| <div style="font-size:12px;color:var(--text-muted);margin-top:2px;">Download PDF reports for any completed scan</div> | |
| </div> | |
| <button class="btn-sm-action btn-scan-secondary" onclick="loadReports()"> | |
| <i class="bi bi-arrow-clockwise"></i> Refresh | |
| </button> | |
| </div> | |
| <!-- PHASE 1: Security Trend Analytics --> | |
| <div style="padding:20px 24px;border-bottom:1px solid var(--border);"> | |
| <div style="font-family:'Syne',sans-serif;font-weight:800;font-size:15px;margin-bottom:4px;"> | |
| <i class="bi bi-graph-up-arrow me-2" style="color:#22c55e;"></i>Security Score Trend | |
| </div> | |
| <div style="font-size:12px;color:var(--text-muted);margin-bottom:12px;">Last 30 days</div> | |
| <div style="position:relative;min-height:180px;"> | |
| <canvas id="securityTrendChart" height="80"></canvas> | |
| <div id="securityTrendEmpty" style="display:none;align-items:center;justify-content:center;flex-direction:column;gap:8px;min-height:180px;color:var(--text-faint);font-size:12px;position:absolute;inset:0;"> | |
| <i class="bi bi-graph-up" style="font-size:28px;"></i> | |
| Run a few scans to see your security score trend | |
| </div> | |
| </div> | |
| <div class="trend-stats-row"> | |
| <span>Average: <strong id="trendAvgScore">β</strong></span> | |
| <span>Best: <strong id="trendBestScore">β</strong></span> | |
| <span>Worst: <strong id="trendWorstScore">β</strong></span> | |
| </div> | |
| </div> | |
| <div class="reports-grid" id="reportsGrid"> | |
| <div style="grid-column:1/-1;text-align:center;padding:40px;color:var(--text-faint);"> | |
| <i class="bi bi-file-earmark-pdf" style="font-size:36px;display:block;margin-bottom:10px;"></i> | |
| Loading your scan history⦠| |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <!-- Detail Drawer --> | |
| <div class="detail-drawer" id="detailDrawer"> | |
| <button class="drawer-close" onclick="closeDrawer()">✕</button> | |
| <div style="font-family:'Syne',sans-serif;font-weight:800;font-size:16px;margin-top:4px;" id="drawerTitle">Finding Details</div> | |
| <div> | |
| <div class="drawer-field-label">Type</div> | |
| <div class="drawer-field-value" id="drawerType"></div> | |
| </div> | |
| <div> | |
| <div class="drawer-field-label">Severity</div> | |
| <div id="drawerSev"></div> | |
| </div> | |
| <div> | |
| <div class="drawer-field-label">Location</div> | |
| <div class="drawer-field-value" id="drawerFile" style="font-family:'JetBrains Mono',monospace;font-size:12px;color:#38bdf8;"></div> | |
| </div> | |
| <div id="drawerMatchWrap"> | |
| <div class="drawer-field-label">Redacted Match</div> | |
| <div class="drawer-field-value" id="drawerMatch" style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#fb7185;"></div> | |
| </div> | |
| <div> | |
| <div class="drawer-field-label">Description</div> | |
| <div class="drawer-field-value" id="drawerDesc"></div> | |
| </div> | |
| <div> | |
| <div class="drawer-field-label">🔧 Remediation</div> | |
| <div class="drawer-fix-box" id="drawerFix"></div> | |
| </div> | |
| <!-- PHASE 1: Compliance mapping (OWASP / NIST) --> | |
| <div id="drawerCompliance" style="display:none;margin-top:6px;"> | |
| <div class="drawer-field-label">Compliance Mapping</div> | |
| <div id="drawerComplianceBadges"></div> | |
| </div> | |
| <!-- PHASE 1: AI Auto-Fix before/after --> | |
| <div id="drawerAutoFix" style="display:none;margin-top:10px;padding-top:10px;border-top:1px solid var(--border);"></div> | |
| </div> | |
| <!-- Toast Area --> | |
| <div class="toast-area" id="toastArea"></div> | |
| <!-- PHASE 1: Chart.js for Security Trend Analytics --> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script> | |
| <script src="api.js"></script> | |
| <script> | |
| var API = 'https://rathious-safeaiscan.hf.space'; | |
| var TOKEN = localStorage.getItem('access_token') || ''; | |
| var lastScanId = null; | |
| /* ββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function esc(s) { | |
| return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); | |
| } | |
| function toast(msg, type) { | |
| type = type || 'info'; | |
| var icons = {success:'bi-check-circle',error:'bi-x-circle',info:'bi-info-circle',warning:'bi-exclamation-triangle'}; | |
| var area = document.getElementById('toastArea'); | |
| var el = document.createElement('div'); | |
| el.className = 'toast-item toast-' + type; | |
| el.innerHTML = '<i class="bi ' + (icons[type]||icons.info) + '"></i><span>' + esc(msg) + '</span>'; | |
| el.onclick = function() { el.remove(); }; | |
| area.appendChild(el); | |
| setTimeout(function() { if (el.parentNode) el.remove(); }, 4000); | |
| } | |
| function setLog(msg, cls) { | |
| var el = document.getElementById('logStrip'); | |
| if (!el) return; | |
| cls = cls || ''; | |
| el.innerHTML = '<span class="' + cls + '">' + esc(msg) + '</span>'; | |
| } | |
| function authHeaders() { | |
| return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + TOKEN }; | |
| } | |
| function authHeadersMultipart() { | |
| return { 'Authorization': 'Bearer ' + TOKEN }; | |
| } | |
| function setProgress(pct) { | |
| var bar = document.getElementById('progressBar'); | |
| if (bar) bar.style.width = pct + '%'; | |
| } | |
| /* ββ Mode switcher ββββββββββββββββββββββββββββββββββββββββββ */ | |
| var MODES = ['code','zip','repo','ai','cve','reports']; | |
| function setMode(mode) { | |
| MODES.forEach(function(m) { | |
| var btn = document.getElementById('mode-' + m); | |
| var sb = document.getElementById('sb-' + m); | |
| var pan = document.getElementById('panel-' + m); | |
| if (btn) btn.classList.toggle('active', m === mode); | |
| if (sb) sb.classList.toggle('active', m === mode); | |
| if (pan) pan.classList.toggle('active', m === mode); | |
| }); | |
| if (mode === 'reports') { | |
| loadReports(); | |
| loadSecurityTrendChart(); // PHASE 1 | |
| } | |
| // Mobile: close the slide-out sidebar after navigating | |
| closeMobileSidebar(); | |
| // Mobile: scroll the active topbar mode button into view (horizontal scroll tabs) | |
| var activeBtn = document.getElementById('mode-' + mode); | |
| if (activeBtn && activeBtn.scrollIntoView) { | |
| activeBtn.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); | |
| } | |
| } | |
| /* ββ MOBILE SIDEBAR TOGGLE ββββββββββββββββββββββββββββββββββββ */ | |
| function toggleMobileSidebar() { | |
| var sidebar = document.getElementById('proSidebar'); | |
| var overlay = document.getElementById('mobileSidebarOverlay'); | |
| if (!sidebar) return; | |
| var isOpen = sidebar.classList.contains('mobile-open'); | |
| if (isOpen) closeMobileSidebar(); | |
| else { | |
| sidebar.classList.add('mobile-open'); | |
| if (overlay) overlay.classList.add('show'); | |
| document.body.classList.add('mobile-nav-open'); | |
| } | |
| } | |
| function closeMobileSidebar() { | |
| var sidebar = document.getElementById('proSidebar'); | |
| var overlay = document.getElementById('mobileSidebarOverlay'); | |
| if (sidebar) sidebar.classList.remove('mobile-open'); | |
| if (overlay) overlay.classList.remove('show'); | |
| document.body.classList.remove('mobile-nav-open'); | |
| } | |
| // Auto-close mobile sidebar if the window is resized/rotated past the | |
| // phone/tablet breakpoint (e.g. rotating a tablet to landscape) | |
| window.addEventListener('resize', function() { | |
| if (window.innerWidth > 768) closeMobileSidebar(); | |
| }); | |
| /* ββ PHASE 1: Security Trend Analytics (Chart.js) ββββββββββββ */ | |
| var securityTrendChart = null; | |
| async function loadSecurityTrendChart() { | |
| var ctx = document.getElementById('securityTrendChart'); | |
| if (!ctx || typeof Chart === 'undefined') return; | |
| try { | |
| var res = await fetch(API + '/api/analytics/security-trend', { headers: authHeaders() }); | |
| var raw = await res.json(); | |
| var data = raw.data || raw; | |
| var points = data.points || []; | |
| var avgEl = document.getElementById('trendAvgScore'); | |
| var bestEl = document.getElementById('trendBestScore'); | |
| var worstEl = document.getElementById('trendWorstScore'); | |
| if (avgEl) avgEl.textContent = data.average_score != null ? data.average_score : 'β'; | |
| if (bestEl) bestEl.textContent = data.best_score != null ? data.best_score : 'β'; | |
| if (worstEl) worstEl.textContent = data.worst_score != null ? data.worst_score : 'β'; | |
| if (securityTrendChart) { securityTrendChart.destroy(); securityTrendChart = null; } | |
| var emptyEl = document.getElementById('securityTrendEmpty'); | |
| if (!points.length) { | |
| if (emptyEl) emptyEl.style.display = 'flex'; | |
| ctx.style.display = 'none'; | |
| return; | |
| } | |
| if (emptyEl) emptyEl.style.display = 'none'; | |
| ctx.style.display = ''; | |
| var gradient = ctx.getContext('2d').createLinearGradient(0, 0, 0, 200); | |
| gradient.addColorStop(0, 'rgba(34,197,94,0.35)'); | |
| gradient.addColorStop(1, 'rgba(34,197,94,0)'); | |
| securityTrendChart = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: points.map(function(p){ return p.date; }), | |
| datasets: [{ | |
| label: 'Security Score', | |
| data: points.map(function(p){ return p.score; }), | |
| borderColor: '#22c55e', | |
| borderWidth: 2.5, | |
| backgroundColor: gradient, | |
| fill: true, | |
| tension: 0.4, | |
| pointBackgroundColor: '#22c55e', | |
| pointRadius: 3, | |
| pointHoverRadius: 6 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| plugins: { | |
| legend: { display: false }, | |
| tooltip: { | |
| backgroundColor: '#0f1a2e', titleColor: '#e8edf8', bodyColor: '#8296b3', | |
| callbacks: { label: function(c) { return ' Score: ' + c.parsed.y + '/100'; } } | |
| } | |
| }, | |
| scales: { | |
| x: { grid: { color: 'rgba(255,255,255,0.04)' }, ticks: { color: '#8296b3', font: { size: 10 } } }, | |
| y: { | |
| min: 0, max: 100, | |
| grid: { color: 'rgba(255,255,255,0.04)' }, | |
| ticks: { color: '#8296b3', font: { size: 10 }, stepSize: 25 } | |
| } | |
| } | |
| } | |
| }); | |
| } catch(e) { | |
| console.debug('Security trend chart load failed (non-fatal):', e.message); | |
| } | |
| } | |
| /* ββ Init βββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| (function init() { | |
| var email = localStorage.getItem('user_email') || ''; | |
| var sideEl = document.getElementById('sidebarEmail'); | |
| if (sideEl && email) sideEl.textContent = email; | |
| // Verify session and get live plan/trial data from backend | |
| fetch(API + '/api/me', { headers: authHeaders() }) | |
| .then(function(r) { | |
| if (r.status === 401) { window.location.replace('login.html'); return null; } | |
| return r.json(); | |
| }) | |
| .then(function(raw) { | |
| if (!raw) return; | |
| var d = raw.data || raw; | |
| // Update email display | |
| if (d.email) { | |
| if (sideEl) sideEl.textContent = d.email; | |
| localStorage.setItem('user_email', d.email); | |
| } | |
| var plan = (d.plan || 'free').toLowerCase(); | |
| var isPro = d.is_pro || false; | |
| var trialActive = d.trial_active || false; | |
| var daysLeft = typeof d.days_left === 'number' ? d.days_left : 0; | |
| var trialEnd = d.trial_end || ''; | |
| // Persist to localStorage for offline use | |
| localStorage.setItem('user_plan', plan); | |
| localStorage.setItem('is_pro', isPro ? 'true' : 'false'); | |
| localStorage.setItem('trial_active', trialActive ? 'true' : 'false'); | |
| localStorage.setItem('trial_days_left', String(daysLeft)); | |
| // Access control: paid pro OR active trial = allowed | |
| var hasAccess = (plan === 'pro' || plan === 'enterprise') || | |
| (plan === 'pro_trial' && trialActive); | |
| if (!hasAccess) { | |
| // Expired trial β tell them exactly why | |
| var reason = (plan === 'pro_trial' && !trialActive) ? 'trial_expired' : 'pro_required'; | |
| window.location.replace('checkout.html?reason=' + reason); | |
| return; | |
| } | |
| // Update PRO badge in topbar to show trial status | |
| updateProBadge(plan, trialActive, daysLeft); | |
| // Show trial countdown banner if in active trial | |
| if (plan === 'pro_trial' && trialActive) { | |
| showTrialBanner(daysLeft, trialEnd); | |
| } | |
| // Update usage pill | |
| var pill = document.getElementById('usagePill'); | |
| if (pill) { | |
| if (plan === 'pro_trial' && trialActive) { | |
| pill.textContent = 'Trial \u2022 ' + daysLeft + 'd left'; | |
| } else { | |
| pill.textContent = 'Unlimited scans'; | |
| } | |
| } | |
| }) | |
| .catch(function(e) { | |
| // Network error β don't redirect, let user keep working | |
| console.warn('[SafeAIScan] Auth check failed (non-fatal):', e.message); | |
| }); | |
| setupZipDrop(); | |
| })(); | |
| function updateProBadge(plan, trialActive, daysLeft) { | |
| var badge = document.querySelector('.pro-badge-tag'); | |
| if (!badge) return; | |
| if (plan === 'pro_trial' && trialActive) { | |
| badge.textContent = 'TRIAL'; | |
| badge.style.background = 'linear-gradient(135deg,rgba(0,255,163,.2),rgba(91,123,254,.15))'; | |
| badge.style.color = '#00ffa3'; | |
| badge.style.border = '1px solid rgba(0,255,163,.35)'; | |
| badge.title = daysLeft + ' days left in your free trial'; | |
| // Show sidebar CTA | |
| var cta = document.getElementById('sidebarTrialCTA'); | |
| var ctaDays = document.getElementById('sidebarTrialDays'); | |
| if (cta) cta.style.display = 'block'; | |
| if (ctaDays) { | |
| var isUrgent = daysLeft <= 3; | |
| ctaDays.innerHTML = isUrgent | |
| ? '<strong style="color:#f43f5e;">' + daysLeft + ' day' + (daysLeft===1?'':'s') + ' left!</strong> Upgrade to keep access.' | |
| : daysLeft + ' day' + (daysLeft===1?'':'s') + ' remaining. All Pro features active.'; | |
| } | |
| } else if (plan === 'enterprise') { | |
| badge.textContent = 'ENTERPRISE'; | |
| badge.style.color = '#e879f9'; | |
| badge.style.background = 'linear-gradient(135deg,rgba(192,38,211,.2),rgba(91,123,254,.15))'; | |
| badge.style.border = '1px solid rgba(192,38,211,.35)'; | |
| } else { | |
| badge.textContent = 'PRO'; | |
| } | |
| } | |
| function showTrialBanner(daysLeft, trialEnd) { | |
| // Don't show if already dismissed this session | |
| if (sessionStorage.getItem('trialBannerDismissed')) return; | |
| if (document.getElementById('trialBanner')) return; | |
| var isUrgent = daysLeft <= 3; | |
| var isWarning = daysLeft <= 7; | |
| var color = isUrgent ? '#f43f5e' : isWarning ? '#fb923c' : '#00ffa3'; | |
| var bgColor = isUrgent | |
| ? 'rgba(244,63,94,.08)' : isWarning | |
| ? 'rgba(251,146,60,.06)' : 'rgba(0,255,163,.06)'; | |
| var borderColor = isUrgent | |
| ? 'rgba(244,63,94,.3)' : isWarning | |
| ? 'rgba(251,146,60,.25)' : 'rgba(0,255,163,.2)'; | |
| var msg = isUrgent | |
| ? '<strong style="color:' + color + ';">Trial ends in ' + daysLeft + ' day' + (daysLeft === 1 ? '' : 's') + '!</strong> Upgrade now to keep all Pro features.' | |
| : 'Your free Pro trial ends in <strong style="color:' + color + ';">' + daysLeft + ' days</strong>' + (trialEnd ? ' (' + trialEnd + ')' : '') + '. All features are fully unlocked.'; | |
| var banner = document.createElement('div'); | |
| banner.id = 'trialBanner'; | |
| banner.style.cssText = [ | |
| 'position:fixed;bottom:16px;left:50%;transform:translateX(-50%);', | |
| 'background:' + bgColor + ';border:1px solid ' + borderColor + ';', | |
| 'border-radius:12px;padding:10px 16px;', | |
| 'display:flex;align-items:center;gap:12px;', | |
| 'font-family:"DM Sans",sans-serif;font-size:12px;color:var(--text-muted);', | |
| 'z-index:9000;box-shadow:0 8px 32px rgba(0,0,0,.5);backdrop-filter:blur(10px);', | |
| 'max-width:90vw;white-space:nowrap;', | |
| 'animation:toastIn .3s ease both;' | |
| ].join(''); | |
| banner.innerHTML = | |
| '<i class="bi bi-clock" style="color:' + color + ';font-size:14px;flex-shrink:0;"></i>' + | |
| '<span>' + msg + '</span>' + | |
| '<a href="checkout.html" style="flex-shrink:0;background:linear-gradient(135deg,#5b7bfe,#4361ee);' + | |
| 'color:#fff;border-radius:8px;padding:5px 14px;font-size:11px;font-weight:600;' + | |
| 'text-decoration:none;white-space:nowrap;">Upgrade \u2014 $1.99/mo</a>' + | |
| '<button onclick="dismissTrialBanner()" style="flex-shrink:0;background:none;border:none;' + | |
| 'color:var(--text-faint);cursor:pointer;font-size:18px;line-height:1;padding:0;">×</button>'; | |
| document.body.appendChild(banner); | |
| } | |
| function dismissTrialBanner() { | |
| var el = document.getElementById('trialBanner'); | |
| if (el) el.remove(); | |
| sessionStorage.setItem('trialBannerDismissed', '1'); | |
| } | |
| function doLogout() { | |
| localStorage.clear(); | |
| window.location.href = 'login.html'; | |
| } | |
| /* ββ CODE SCANNER βββββββββββββββββββββββββββββββββββββββββββ */ | |
| async function runCodeScan() { | |
| var code = (document.getElementById('codeInput') || {}).value || ''; | |
| if (!code.trim()) { toast('Paste some code first', 'warning'); return; } | |
| var btn = document.getElementById('scanBtn'); | |
| if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Scanningβ¦'; } | |
| setProgress(20); setLog('Scanning for secretsβ¦'); | |
| try { | |
| var res = await fetch(API + '/api/analyze', { | |
| method: 'POST', headers: authHeaders(), | |
| body: JSON.stringify({ text: code }) | |
| }); | |
| setProgress(70); | |
| var raw = await res.json(); | |
| var data = raw.data || raw; | |
| if (!res.ok) { | |
| toast(data.error || data.detail || 'Scan failed', 'error'); | |
| setLog('Scan failed', 'log-err'); | |
| return; | |
| } | |
| var findings = data.findings || []; | |
| lastScanId = data.id || data.scan_id || null; | |
| window._lastScanFull = data; // PHASE 1: cache full result for executive PDF export | |
| renderFindings(findings, data.risk || data.risk_level || 'NONE'); | |
| // PHASE 1: weighted security score ring | |
| if (typeof data.security_score === 'number') { | |
| renderSecurityScoreInline(data.security_score, data.score_risk_level); | |
| } | |
| setProgress(100); | |
| setLog('Scan complete β ' + findings.length + ' secret(s) found', findings.length ? 'log-warn' : 'log-ok'); | |
| toast(findings.length + ' finding(s) Β· risk: ' + (data.risk || data.risk_level || 'NONE'), | |
| findings.length ? 'warning' : 'success'); | |
| if (data.ai) showInlineAI(data.ai); | |
| } catch(e) { | |
| toast('Network error: ' + e.message, 'error'); | |
| setLog(e.message, 'log-err'); | |
| } finally { | |
| setTimeout(function() { setProgress(0); }, 800); | |
| if (btn) { btn.disabled = false; btn.innerHTML = '<i class="bi bi-play-circle-fill"></i> Run Scan'; } | |
| } | |
| } | |
| /* ββ PHASE 1: Security Score Ring ββββββββββββββββββββββββββββ */ | |
| function renderSecurityScoreInline(score, riskLevel) { | |
| var card = document.getElementById('securityScoreCard'); | |
| var ring = document.getElementById('scoreRing'); | |
| var value = document.getElementById('scoreRingValue'); | |
| var label = document.getElementById('scoreRiskLabel'); | |
| if (!card) return; | |
| var colorMap = { | |
| 'Excellent': '#22c55e', 'Good': '#22c55e', | |
| 'Moderate': '#facc15', 'High Risk': '#fb923c', 'Critical': '#c026d3' | |
| }; | |
| var color = colorMap[riskLevel] || '#facc15'; | |
| var circumference = 2 * Math.PI * 24; // r=24 | |
| var pct = Math.max(0, Math.min(100, score)); | |
| var offset = circumference * (1 - pct / 100); | |
| card.style.display = 'block'; | |
| if (ring) { | |
| ring.style.stroke = color; | |
| ring.setAttribute('stroke-dasharray', circumference.toFixed(2)); | |
| ring.setAttribute('stroke-dashoffset', offset.toFixed(2)); | |
| } | |
| if (value) { value.textContent = score; value.style.color = color; } | |
| if (label) { label.textContent = riskLevel || 'β'; label.style.color = color; } | |
| } | |
| function clearCode() { | |
| var el = document.getElementById('codeInput'); | |
| if (el) el.value = ''; | |
| renderFindings([], 'NONE'); | |
| setLog('Ready', 'log-ok'); | |
| } | |
| /* ββ AI ONLY (code panel) βββββββββββββββββββββββββββββββββββ */ | |
| async function runAIOnly() { | |
| var code = (document.getElementById('codeInput') || {}).value || ''; | |
| if (!code.trim()) { toast('Paste some code first', 'warning'); return; } | |
| var btn = document.getElementById('aiOnlyBtn'); | |
| if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Analysingβ¦'; } | |
| setProgress(30); setLog('Running deep AI analysisβ¦'); | |
| try { | |
| var res = await fetch(API + '/api/analyze', { | |
| method: 'POST', headers: authHeaders(), | |
| body: JSON.stringify({ text: code, mode: 'ai_only', depth: 'full' }) | |
| }); | |
| setProgress(80); | |
| var raw = await res.json(); | |
| var data = raw.data || raw; | |
| if (!res.ok) { toast(data.error || 'AI analysis failed', 'error'); return; } | |
| var ai = data.ai || data; | |
| setMode('ai'); | |
| var codeEl = document.getElementById('aiCodeInput'); | |
| if (codeEl) codeEl.value = code; | |
| displayAIResult(ai); | |
| setProgress(100); | |
| toast('AI analysis complete', 'success'); | |
| } catch(e) { | |
| toast('AI error: ' + e.message, 'error'); | |
| } finally { | |
| setTimeout(function() { setProgress(0); }, 800); | |
| if (btn) { btn.disabled = false; btn.innerHTML = '<i class="bi bi-cpu"></i> AI Only'; } | |
| } | |
| } | |
| /* ββ FINDINGS RENDERER ββββββββββββββββββββββββββββββββββββββ */ | |
| function renderFindings(findings, risk) { | |
| var list = document.getElementById('resultsList'); | |
| var rb = document.getElementById('riskBadge'); | |
| if (!list) return; | |
| // Update risk badge β PHASE 1: added CRITICAL | |
| if (rb) { | |
| var riskClass = { 'CRITICAL':'risk-critical','HIGH':'risk-high','MEDIUM':'risk-medium','LOW':'risk-low','NONE':'risk-none' }; | |
| rb.className = 'risk-badge ' + (riskClass[risk] || 'risk-none'); | |
| rb.textContent = risk || 'NONE'; | |
| } | |
| // Update summary β PHASE 1: count CRITICAL into the HIGH bucket display | |
| // (keeps the existing 4-stat layout; CRITICAL findings still show in the list) | |
| var counts = {CRITICAL:0,HIGH:0,MEDIUM:0,LOW:0}; | |
| (findings||[]).forEach(function(f) { counts[f.severity] = (counts[f.severity]||0)+1; }); | |
| var sumTotal = document.getElementById('sumTotal'); | |
| var sumHigh = document.getElementById('sumHigh'); | |
| var sumMed = document.getElementById('sumMed'); | |
| var sumLow = document.getElementById('sumLow'); | |
| if (sumTotal) sumTotal.textContent = (findings||[]).length; | |
| if (sumHigh) sumHigh.textContent = counts.CRITICAL + counts.HIGH; // CRITICAL folded into HIGH stat | |
| if (sumMed) sumMed.textContent = counts.MEDIUM; | |
| if (sumLow) sumLow.textContent = counts.LOW; | |
| if (!findings || !findings.length) { | |
| list.innerHTML = '<div class="results-empty"><i class="bi bi-shield-check" style="font-size:32px;color:var(--sg);display:block;margin-bottom:8px;"></i>No secrets found β code looks clean!</div>'; | |
| return; | |
| } | |
| var sevOrder = {CRITICAL:4,HIGH:3,MEDIUM:2,LOW:1}; | |
| var sorted = findings.slice().sort(function(a,b){ return (sevOrder[b.severity]||0) - (sevOrder[a.severity]||0); }); | |
| list.innerHTML = sorted.map(function(f, i) { | |
| var sevClass = 'sev-' + (f.severity||'LOW'); | |
| var file = f.file ? (esc(f.file) + (f.line ? ':' + f.line : '')) : ''; | |
| var autoFixBadge = f.auto_fix ? '<span class="badge-pill" style="background:rgba(34,197,94,0.12);color:#22c55e;border:1px solid rgba(34,197,94,0.25);font-size:9px;margin-left:6px;"><i class="bi bi-magic"></i> Fix available</span>' : ''; | |
| return '<div class="finding-card" onclick="openDrawer(' + i + ')">' + | |
| '<span class="sev-chip ' + sevClass + '">' + esc(f.severity||'?') + '</span>' + | |
| '<div class="finding-type">' + esc(f.type || f.title || 'Secret') + autoFixBadge + '</div>' + | |
| (file ? '<div class="finding-meta">' + file + '</div>' : '') + | |
| (f.match ? '<div class="finding-match">' + esc(f.match) + '</div>' : '') + | |
| '</div>'; | |
| }).join(''); | |
| // Store sorted order for drawer (matches displayed index) | |
| window._proFindings = sorted; | |
| } | |
| /* ββ INLINE AI PANEL (code scan) βββββββββββββββββββββββββββ */ | |
| function showInlineAI(ai) { | |
| if (!ai || !ai.explanation) return; | |
| // If we're in code panel, show a brief note in the log | |
| setLog('AI: ' + (ai.explanation||'').substring(0,80) + 'β¦', 'log-ok'); | |
| } | |
| /* ββ DRAWER βββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function openDrawer(idx) { | |
| var f = (window._proFindings || [])[idx]; | |
| if (!f) return; | |
| document.getElementById('drawerTitle').textContent = f.type || f.title || 'Finding'; | |
| document.getElementById('drawerType').textContent = f.type || f.title || 'β'; | |
| document.getElementById('drawerFile').textContent = f.file ? (f.file + (f.line ? ' Β· line ' + f.line : '')) : 'β'; | |
| document.getElementById('drawerMatch').textContent = f.match || 'β'; | |
| document.getElementById('drawerDesc').textContent = f.description || 'β'; | |
| document.getElementById('drawerFix').textContent = f.fix || 'β'; | |
| var sevEl = document.getElementById('drawerSev'); | |
| var sevClass = {CRITICAL:'sev-CRITICAL',HIGH:'sev-HIGH',MEDIUM:'sev-MEDIUM',LOW:'sev-LOW'}; | |
| sevEl.innerHTML = '<span class="sev-chip ' + (sevClass[f.severity]||'sev-LOW') + '">' + esc(f.severity||'?') + '</span>'; | |
| // PHASE 1: Compliance mapping (OWASP / NIST) | |
| var compEl = document.getElementById('drawerCompliance'); | |
| var compBadges = document.getElementById('drawerComplianceBadges'); | |
| if (compEl && compBadges) { | |
| if (f.owasp || f.nist) { | |
| compEl.style.display = 'block'; | |
| compBadges.innerHTML = | |
| (f.owasp ? '<span class="badge-pill" style="background:rgba(91,123,254,0.12);color:#5b7bfe;border:1px solid rgba(91,123,254,0.25);margin-right:6px;">' + esc(f.owasp) + '</span>' : '') + | |
| (f.nist ? '<span class="badge-pill" style="background:rgba(56,189,248,0.1);color:#38bdf8;border:1px solid rgba(56,189,248,0.2);">NIST ' + esc(f.nist) + '</span>' : ''); | |
| } else { | |
| compEl.style.display = 'none'; | |
| compBadges.innerHTML = ''; | |
| } | |
| } | |
| // PHASE 1: AI Auto-Fix before/after diff | |
| var autoFixEl = document.getElementById('drawerAutoFix'); | |
| if (autoFixEl) { | |
| if (f.auto_fix && f.auto_fix.before) { | |
| var af = f.auto_fix; | |
| var conf = af.confidence || 0; | |
| var confColor = conf >= 90 ? '#22c55e' : conf >= 70 ? '#facc15' : '#fb923c'; | |
| autoFixEl.style.display = 'block'; | |
| autoFixEl.innerHTML = | |
| '<div style="font-size:10px;text-transform:uppercase;letter-spacing:0.8px;color:#22c55e;margin-bottom:6px;display:flex;justify-content:space-between;align-items:center;">' + | |
| '<span><i class="bi bi-magic me-1"></i>AI Suggested Fix</span>' + | |
| '<span class="badge-pill" style="color:' + confColor + ';border:1px solid ' + confColor + '33;">' + conf + '% confidence</span>' + | |
| '</div>' + | |
| '<div style="font-size:9px;text-transform:uppercase;color:#f43f5e;margin-bottom:3px;">Before</div>' + | |
| '<pre style="background:rgba(244,63,94,0.06);border:1px solid rgba(244,63,94,0.18);border-radius:6px;padding:8px;font-size:11px;overflow-x:auto;white-space:pre-wrap;word-break:break-word;margin:0 0 8px;">' + esc(af.before) + '</pre>' + | |
| '<div style="font-size:9px;text-transform:uppercase;color:#22c55e;margin-bottom:3px;">After</div>' + | |
| '<pre style="background:rgba(34,197,94,0.06);border:1px solid rgba(34,197,94,0.18);border-radius:6px;padding:8px;font-size:11px;overflow-x:auto;white-space:pre-wrap;word-break:break-word;margin:0 0 8px;">' + esc(af.after) + '</pre>' + | |
| '<div style="font-size:12px;color:var(--text-muted);line-height:1.5;">' + esc(af.explanation||'') + '</div>'; | |
| } else { | |
| autoFixEl.style.display = 'none'; | |
| autoFixEl.innerHTML = ''; | |
| } | |
| } | |
| document.getElementById('detailDrawer').classList.add('open'); | |
| } | |
| function closeDrawer() { | |
| var drawer = document.getElementById('detailDrawer'); | |
| drawer.classList.remove('open'); | |
| drawer.style.transform = ''; | |
| drawer.style.transition = ''; | |
| } | |
| /* ββ MOBILE: Swipe-down-to-close for the detail drawer βββββββ */ | |
| (function setupDrawerSwipe() { | |
| var drawer = document.getElementById('detailDrawer'); | |
| if (!drawer) return; | |
| var startY = 0, currentY = 0, dragging = false; | |
| drawer.addEventListener('touchstart', function(e) { | |
| // Only start swipe-to-close if touch begins near the top of the drawer | |
| // (avoids hijacking scroll gestures inside the drawer content) | |
| if (e.touches[0].clientY - drawer.getBoundingClientRect().top > 60) return; | |
| startY = e.touches[0].clientY; | |
| dragging = true; | |
| drawer.style.transition = 'none'; | |
| }, { passive: true }); | |
| drawer.addEventListener('touchmove', function(e) { | |
| if (!dragging) return; | |
| currentY = e.touches[0].clientY; | |
| var delta = Math.max(0, currentY - startY); | |
| if (delta > 0) drawer.style.transform = 'translateY(' + delta + 'px)'; | |
| }, { passive: true }); | |
| drawer.addEventListener('touchend', function() { | |
| if (!dragging) return; | |
| dragging = false; | |
| drawer.style.transition = ''; | |
| var delta = Math.max(0, currentY - startY); | |
| drawer.style.transform = ''; | |
| if (delta > 100) closeDrawer(); | |
| startY = 0; currentY = 0; | |
| }); | |
| })(); | |
| /* ββ ZIP UPLOAD βββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function setupZipDrop() { | |
| var zone = document.getElementById('dropZone'); | |
| var input = document.getElementById('zipInput'); | |
| if (!zone || !input) return; | |
| zone.addEventListener('dragover', function(e) { e.preventDefault(); zone.classList.add('drag-over'); }); | |
| zone.addEventListener('dragleave', function() { zone.classList.remove('drag-over'); }); | |
| zone.addEventListener('drop', function(e) { | |
| e.preventDefault(); zone.classList.remove('drag-over'); | |
| var file = e.dataTransfer.files[0]; | |
| if (file) handleZipFile(file); | |
| }); | |
| input.addEventListener('change', function() { | |
| if (this.files[0]) handleZipFile(this.files[0]); | |
| }); | |
| } | |
| async function handleZipFile(file) { | |
| if (!file.name.toLowerCase().endsWith('.zip')) { | |
| toast('Only .zip files are accepted', 'warning'); return; | |
| } | |
| var prog = document.getElementById('zipProgress'); | |
| var fill = document.getElementById('zipProgressFill'); | |
| var label = document.getElementById('zipStateLabel'); | |
| var results= document.getElementById('zipResults'); | |
| prog.classList.add('show'); | |
| if (fill) fill.style.width = '20%'; | |
| if (label) label.textContent = 'Uploadingβ¦'; | |
| if (results) results.innerHTML = ''; | |
| var fd = new FormData(); | |
| fd.append('file', file); | |
| try { | |
| var res = await fetch(API + '/scan/file', { | |
| method: 'POST', headers: authHeadersMultipart(), body: fd | |
| }); | |
| if (fill) fill.style.width = '80%'; | |
| var raw = await res.json(); | |
| var data = raw.data || raw; | |
| if (!res.ok) { | |
| toast(data.error || data.detail || 'Upload failed', 'error'); | |
| prog.classList.remove('show'); | |
| return; | |
| } | |
| if (fill) fill.style.width = '100%'; | |
| if (label) label.textContent = 'Scan complete!'; | |
| var findings = data.findings || []; | |
| lastScanId = data.scan_id || data.id || null; | |
| window._proFindings = findings; | |
| if (results) results.innerHTML = buildResultsBlock(findings, data.risk_level || 'NONE', data.scan_id); | |
| toast('ZIP scan: ' + findings.length + ' secret(s) found [' + (data.risk_level||'NONE') + ']', | |
| findings.length ? 'warning' : 'success'); | |
| setTimeout(function() { prog.classList.remove('show'); }, 2000); | |
| } catch(e) { | |
| toast('Upload error: ' + e.message, 'error'); | |
| prog.classList.remove('show'); | |
| } | |
| } | |
| /* ββ REPO SCAN ββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| async function runRepoScan() { | |
| var url = (document.getElementById('repoUrl') || {}).value || ''; | |
| url = url.trim(); | |
| if (!url || url === 'https://github.com/') { | |
| toast('Enter a valid GitHub repo URL', 'warning'); return; | |
| } | |
| if (!url.startsWith('https://github.com/')) { | |
| toast('Only https://github.com/ URLs are supported', 'warning'); return; | |
| } | |
| if (url.split('/').filter(Boolean).length < 4) { | |
| toast('Enter a full repo URL: https://github.com/owner/repo', 'warning'); return; | |
| } | |
| var prog = document.getElementById('repoProgress'); | |
| var fill = document.getElementById('repoProgressFill'); | |
| var label = document.getElementById('repoStateLabel'); | |
| var msgEl = document.getElementById('repoProgressMsg'); | |
| var results = document.getElementById('repoResults'); | |
| prog.classList.add('show'); | |
| if (fill) fill.style.width = '5%'; | |
| if (label) label.textContent = 'Queuing scanβ¦'; | |
| if (results) results.innerHTML = ''; | |
| try { | |
| var res = await fetch(API + '/scan/repo', { | |
| method: 'POST', headers: authHeaders(), | |
| body: JSON.stringify({ repo_url: url }) | |
| }); | |
| var raw = await res.json(); | |
| var data = raw.data || raw; | |
| if (!res.ok) { | |
| toast(data.error || data.detail || 'Failed to queue scan', 'error'); | |
| prog.classList.remove('show'); return; | |
| } | |
| var taskId = data.task_id; | |
| if (label) label.textContent = 'Scan queued β pollingβ¦'; | |
| pollRepoTask(taskId, fill, label, msgEl, prog, results); | |
| } catch(e) { | |
| toast('Error: ' + e.message, 'error'); | |
| prog.classList.remove('show'); | |
| } | |
| } | |
| function pollRepoTask(taskId, fill, label, msgEl, prog, results) { | |
| var attempts = 0; | |
| var maxAttempts = 120; // 6 minutes | |
| var iv = setInterval(async function() { | |
| attempts++; | |
| if (attempts > maxAttempts) { | |
| clearInterval(iv); | |
| toast('Scan timed out', 'error'); | |
| prog.classList.remove('show'); | |
| return; | |
| } | |
| try { | |
| var res = await fetch(API + '/scan/status/' + taskId, { headers: authHeaders() }); | |
| var raw = await res.json(); | |
| var task = raw.data || raw; | |
| var state = task.state || 'QUEUED'; | |
| var progress = task.progress || 0; | |
| var message = task.message || state; | |
| if (fill) fill.style.width = Math.max(progress, 8) + '%'; | |
| if (label) label.textContent = message; | |
| if (msgEl) msgEl.textContent = state; | |
| if (state === 'DONE') { | |
| clearInterval(iv); | |
| var r = task.result_json || task.result || {}; | |
| var findings = r.findings || []; | |
| lastScanId = r.scan_id || taskId; | |
| window._proFindings = findings; | |
| window._lastScanFull = r; // PHASE 1: cache full result for executive PDF export | |
| if (results) results.innerHTML = buildResultsBlock(findings, r.risk_level || 'NONE', lastScanId, r); | |
| toast('Repo scan done: ' + findings.length + ' secret(s) [' + (r.risk_level||'NONE') + ']', | |
| findings.length ? 'warning' : 'success'); | |
| if (fill) fill.style.width = '100%'; | |
| setTimeout(function() { prog.classList.remove('show'); }, 2000); | |
| } else if (state === 'FAILED') { | |
| clearInterval(iv); | |
| var errMsg = (task.result_json && task.result_json.error) || task.message || 'Scan failed'; | |
| toast(errMsg, 'error'); | |
| prog.classList.remove('show'); | |
| } | |
| } catch(e) { | |
| // Ignore network blips during polling | |
| } | |
| }, 3000); | |
| } | |
| /* ββ RESULTS BLOCK BUILDER ββββββββββββββββββββββββββββββββββ */ | |
| function buildResultsBlock(findings, risk, scanId, repoResult) { | |
| var riskColor = {CRITICAL:'#c026d3',HIGH:'#f43f5e',MEDIUM:'#fb923c',LOW:'#fbbf24',NONE:'#34d399'}; | |
| var html = ''; | |
| // PHASE 1: Repository Health Dashboard card | |
| if (repoResult && repoResult.repo_health) { | |
| html += buildRepoHealthCard(repoResult.repo_health); | |
| } | |
| html += '<div style="background:var(--bg-2);border:1px solid var(--border);border-radius:14px;overflow:hidden;">' + | |
| '<div style="padding:14px 18px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;">' + | |
| '<span style="font-size:13px;font-weight:600;color:var(--text-primary);">' + findings.length + ' secret(s) found</span>' + | |
| '<span style="font-size:11px;font-weight:700;padding:3px 10px;border-radius:99px;background:rgba(255,255,255,.06);color:' + (riskColor[risk]||'#34d399') + ';">' + esc(risk) + '</span>'; | |
| if (scanId) { | |
| html += '<button onclick="downloadPDF(\'' + esc(scanId) + '\')" style="margin-left:auto;background:none;border:1px solid var(--border);color:var(--text-muted);border-radius:8px;padding:5px 12px;font-size:11px;cursor:pointer;" title="Download PDF">' + | |
| '<i class="bi bi-file-earmark-pdf me-1"></i>PDF</button>'; | |
| html += '<button onclick="exportExecutivePDF()" style="background:none;border:1px solid var(--border);color:var(--text-muted);border-radius:8px;padding:5px 12px;font-size:11px;cursor:pointer;" title="Download Executive Report">' + | |
| '<i class="bi bi-file-earmark-richtext me-1"></i>Executive</button>'; | |
| } | |
| html += '</div>'; | |
| if (!findings.length) { | |
| html += '<div style="padding:24px;text-align:center;color:var(--sg);">' + | |
| '<i class="bi bi-shield-check" style="font-size:28px;display:block;margin-bottom:6px;"></i>No secrets detected!</div>'; | |
| } else { | |
| html += '<div style="padding:12px;">' + | |
| findings.map(function(f, i) { | |
| var sevColors = {CRITICAL:'#e879f9',HIGH:'#fb7185',MEDIUM:'#fdba74',LOW:'#fbbf24'}; | |
| var autoFixBadge = f.auto_fix ? ' <span class="badge-pill" style="background:rgba(34,197,94,0.12);color:#22c55e;border:1px solid rgba(34,197,94,0.25);">Fix available</span>' : ''; | |
| return '<div style="padding:10px;border-bottom:1px solid rgba(99,130,255,.06);cursor:pointer;" ' + | |
| 'onclick="openDrawer(' + i + ')">' + | |
| '<span style="font-size:9px;font-weight:700;text-transform:uppercase;color:' + (sevColors[f.severity]||'#fbbf24') + ';margin-right:8px;">' + esc(f.severity) + '</span>' + | |
| '<span style="font-size:12px;font-weight:600;color:var(--text-primary);">' + esc(f.type||f.title||'Secret') + '</span>' + autoFixBadge + | |
| (f.file ? '<div style="font-family:\'JetBrains Mono\',monospace;font-size:10px;color:#38bdf8;margin-top:2px;">' + esc(f.file) + (f.line?':'+f.line:'') + '</div>' : '') + | |
| '</div>'; | |
| }).join('') + '</div>'; | |
| } | |
| html += '</div>'; | |
| window._proFindings = findings; | |
| return html; | |
| } | |
| /* ββ PHASE 1: Repository Health Dashboard card βββββββββββββββ */ | |
| function buildRepoHealthCard(health) { | |
| var colorMap = { | |
| 'Excellent':'#22c55e','Good':'#22c55e','Moderate':'#facc15','High Risk':'#fb923c','Critical':'#c026d3' | |
| }; | |
| var scoreColor = colorMap[health.risk_level] || '#facc15'; | |
| var stats = [ | |
| { label: 'Security Score', value: (health.security_score != null ? health.security_score : 0), color: scoreColor }, | |
| { label: 'Critical', value: health.critical_count || 0, color: '#c026d3' }, | |
| { label: 'High', value: health.high_count || 0, color: '#f43f5e' }, | |
| { label: 'Medium', value: health.medium_count || 0, color: '#fb923c' }, | |
| { label: 'Low', value: health.low_count || 0, color: '#facc15' }, | |
| { label: 'Secrets Found', value: health.secret_count || 0, color: '#5b7bfe' }, | |
| { label: 'Dependencies', value: health.dependency_count || 0, color: '#38bdf8' }, | |
| { label: 'Outdated', value: health.outdated_packages || 0, color: '#fb923c' }, | |
| ]; | |
| var cells = stats.map(function(s) { | |
| return '<div style="text-align:center;background:var(--bg-1);border:1px solid var(--border);border-radius:10px;padding:12px 8px;">' + | |
| '<div style="font-family:\'Syne\',sans-serif;font-weight:800;font-size:20px;color:' + s.color + ';">' + s.value + '</div>' + | |
| '<div style="font-size:10px;text-transform:uppercase;letter-spacing:0.6px;color:var(--text-faint);margin-top:4px;">' + esc(s.label) + '</div>' + | |
| '</div>'; | |
| }).join(''); | |
| return '<div style="background:var(--bg-2);border:1px solid var(--border);border-radius:14px;padding:16px;margin-bottom:12px;">' + | |
| '<div style="font-family:\'Syne\',sans-serif;font-weight:700;font-size:14px;margin-bottom:12px;">' + | |
| '<i class="bi bi-heart-pulse me-2" style="color:var(--accent);"></i>Repository Health</div>' + | |
| '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(90px,1fr));gap:10px;">' + cells + '</div>' + | |
| '</div>'; | |
| } | |
| /* ββ DEEP AI PANEL ββββββββββββββββββββββββββββββββββββββββββ */ | |
| async function runDeepAI() { | |
| var code = (document.getElementById('aiCodeInput') || {}).value || ''; | |
| if (!code.trim()) { toast('Paste code for AI analysis', 'warning'); return; } | |
| var btn = document.getElementById('aiAnalyzeBtn'); | |
| if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Analysingβ¦'; } | |
| try { | |
| var res = await fetch(API + '/api/analyze', { | |
| method: 'POST', headers: authHeaders(), | |
| body: JSON.stringify({ text: code, mode: 'ai_only', depth: 'full' }) | |
| }); | |
| var raw = await res.json(); | |
| var data = raw.data || raw; | |
| if (!res.ok) { toast(data.error || 'AI failed', 'error'); return; } | |
| displayAIResult(data.ai || data); | |
| toast('AI analysis complete', 'success'); | |
| } catch(e) { | |
| toast('AI error: ' + e.message, 'error'); | |
| } finally { | |
| if (btn) { btn.disabled = false; btn.innerHTML = '<i class="bi bi-cpu"></i> Analyse with AI'; } | |
| } | |
| } | |
| function displayAIResult(ai) { | |
| if (!ai) return; | |
| var section = document.getElementById('aiResult'); | |
| var expEl = document.getElementById('aiExplanation'); | |
| var fixEl = document.getElementById('aiFixesList'); | |
| var fixLbl = document.getElementById('aiFixesLabel'); | |
| var riskRow = document.getElementById('aiRiskRow'); | |
| var sevEl = document.getElementById('aiSeverity'); | |
| if (section) section.style.display = 'block'; | |
| if (expEl) expEl.textContent = ai.explanation || 'Analysis complete.'; | |
| if (fixEl && ai.fixes && ai.fixes.length) { | |
| if (fixLbl) fixLbl.style.display = 'block'; | |
| fixEl.innerHTML = ai.fixes.map(function(fix, i) { | |
| return '<div class="ai-fix-item"><div class="fix-num">' + (i+1) + '</div><div class="fix-text">' + esc(fix) + '</div></div>'; | |
| }).join(''); | |
| } | |
| if (riskRow && ai.severity_prediction) { | |
| riskRow.style.display = 'block'; | |
| if (sevEl) sevEl.textContent = ai.severity_prediction; | |
| } | |
| } | |
| /* ββ CVE LOOKUP βββββββββββββββββββββββββββββββββββββββββββββ */ | |
| async function runCVELookup() { | |
| var query = (document.getElementById('cveInput') || {}).value || ''; | |
| if (!query.trim()) { toast('Enter a CVE ID or keyword', 'warning'); return; } | |
| var results = document.getElementById('cveResults'); | |
| if (results) results.innerHTML = '<div style="color:var(--text-faint);font-size:12px;padding:8px 0;">Searchingβ¦</div>'; | |
| try { | |
| var res = await fetch(API + '/api/cve/search?query=' + encodeURIComponent(query.trim()), { headers: authHeaders() }); | |
| var raw = await res.json(); | |
| var data = raw.data || raw; | |
| if (!res.ok) { | |
| if (results) results.innerHTML = '<div style="color:var(--danger);font-size:12px;">CVE lookup unavailable. The CVE API may not be enabled on this plan or endpoint.</div>'; | |
| return; | |
| } | |
| var cves = data.cves || data.data || []; | |
| if (!cves.length) { | |
| if (results) results.innerHTML = '<div style="color:var(--text-muted);font-size:12px;padding:8px 0;">No CVEs found for "' + esc(query) + '"</div>'; | |
| return; | |
| } | |
| if (results) results.innerHTML = cves.map(function(cve) { | |
| var score = cve.cvss || cve.score; | |
| var sev = score >= 9 ? 'HIGH' : score >= 7 ? 'HIGH' : score >= 4 ? 'MEDIUM' : 'LOW'; | |
| var sevColors = {HIGH:'#fb7185',MEDIUM:'#fdba74',LOW:'#fbbf24'}; | |
| return '<div class="cve-result-card">' + | |
| '<div style="display:flex;align-items:center;justify-content:space-between;">' + | |
| '<span class="cve-id">' + esc(cve.id||'') + '</span>' + | |
| (score != null ? '<span class="cve-score-badge" style="background:rgba(255,255,255,.06);color:' + (sevColors[sev]||'#fbbf24') + '">CVSS ' + score + '</span>' : '') + | |
| '</div>' + | |
| '<div class="cve-desc">' + esc((cve.description||'').substring(0,220)) + '</div>' + | |
| '</div>'; | |
| }).join(''); | |
| } catch(e) { | |
| if (results) results.innerHTML = '<div style="color:var(--danger);font-size:12px;">Error: ' + esc(e.message) + '</div>'; | |
| } | |
| } | |
| /* ββ PDF REPORTS ββββββββββββββββββββββββββββββββββββββββββββ */ | |
| async function loadReports() { | |
| var grid = document.getElementById('reportsGrid'); | |
| if (grid) grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:32px;color:var(--text-faint);"><span class="spinner-border spinner-border-sm me-2"></span>Loadingβ¦</div>'; | |
| try { | |
| var res = await fetch(API + '/scan/history', { headers: authHeaders() }); | |
| var raw = await res.json(); | |
| var data = raw.data || raw; | |
| var scans = Array.isArray(data) ? data : (data.scans || data.history || []); | |
| if (!scans.length) { | |
| if (grid) grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:40px;color:var(--text-faint);"><i class="bi bi-file-earmark-pdf" style="font-size:36px;display:block;margin-bottom:10px;opacity:.4;"></i>No scans yet. Run your first scan to see reports here.</div>'; | |
| return; | |
| } | |
| if (grid) grid.innerHTML = scans.map(function(s) { | |
| var riskColors = {HIGH:'#f43f5e',MEDIUM:'#fb923c',LOW:'#fbbf24',NONE:'#34d399'}; | |
| var risk = s.risk_level || s.risk || 'NONE'; | |
| var date = s.created_at ? new Date(s.created_at).toLocaleDateString() : 'β'; | |
| return '<div class="report-card">' + | |
| '<div style="display:flex;align-items:center;justify-content:space-between;">' + | |
| '<span class="report-risk" style="color:' + (riskColors[risk]||'#34d399') + ';">' + esc(risk) + '</span>' + | |
| '<span class="report-meta">' + esc(date) + '</span>' + | |
| '</div>' + | |
| '<div class="report-source" title="' + esc(s.source||'') + '">' + esc(s.source || 'scan') + '</div>' + | |
| '<div style="font-size:12px;color:var(--text-muted);">' + (s.total_secrets||0) + ' secret(s) found</div>' + | |
| '<button onclick="downloadPDF(\'' + esc(s.id) + '\')" style="background:linear-gradient(135deg,#5b7bfe,#4361ee);color:#fff;border:none;border-radius:8px;padding:8px 14px;font-size:12px;font-weight:600;cursor:pointer;margin-top:4px;">' + | |
| '<i class="bi bi-file-earmark-pdf me-1"></i>Download PDF</button>' + | |
| '</div>'; | |
| }).join(''); | |
| } catch(e) { | |
| if (grid) grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:32px;color:var(--danger);">Failed to load reports: ' + esc(e.message) + '</div>'; | |
| } | |
| } | |
| async function downloadPDF(scanId) { | |
| toast('Generating PDFβ¦', 'info'); | |
| try { | |
| var res = await fetch(API + '/report/' + scanId + '/pdf', { headers: authHeaders() }); | |
| if (!res.ok) { | |
| var raw = await res.json().catch(function(){return{};}); | |
| toast(raw.detail || 'PDF generation failed', 'error'); return; | |
| } | |
| var blob = await res.blob(); | |
| var url = URL.createObjectURL(blob); | |
| var a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'secretscan-' + scanId.substring(0,8) + '.pdf'; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| toast('PDF downloaded!', 'success'); | |
| } catch(e) { | |
| toast('PDF error: ' + e.message, 'error'); | |
| } | |
| } | |
| async function exportCurrentPDF() { | |
| if (!lastScanId) { toast('Run a scan first to generate a PDF', 'warning'); return; } | |
| downloadPDF(lastScanId); | |
| } | |
| /* ββ PHASE 1: Executive PDF Report βββββββββββββββββββββββββββ */ | |
| async function exportExecutivePDF() { | |
| var data = window._lastScanFull; | |
| if (!data || !(data.findings || []).length) { | |
| toast('Run a scan first to generate an executive report', 'warning'); | |
| return; | |
| } | |
| toast('Generating executive reportβ¦', 'info'); | |
| try { | |
| var payload = { | |
| findings: data.findings || [], | |
| scan_id: data.id || data.scan_id || '', | |
| risk_level: data.risk || data.risk_level || 'NONE', | |
| total_secrets: data.total_secrets || (data.findings || []).length, | |
| summary: data.summary || {}, | |
| source: data.source || '', | |
| truncated: data.truncated || false, | |
| security_score: data.security_score || 0, | |
| score_risk_level: data.score_risk_level || 'Moderate', | |
| repo_health: data.repo_health || null, | |
| dependency_findings: data.dependency_findings || [] | |
| }; | |
| var res = await fetch(API + '/api/report/pdf/executive', { | |
| method: 'POST', headers: authHeaders(), | |
| body: JSON.stringify(payload) | |
| }); | |
| if (res.status === 403) { | |
| var raw = await res.json().catch(function(){return{};}); | |
| var msg = (raw.detail && raw.detail.error) || raw.error || 'Executive PDF reports require a Pro or Enterprise plan.'; | |
| toast(msg, 'warning'); | |
| return; | |
| } | |
| if (!res.ok) { | |
| toast('Executive PDF generation failed', 'error'); | |
| return; | |
| } | |
| var blob = await res.blob(); | |
| var url = URL.createObjectURL(blob); | |
| var a = document.createElement('a'); | |
| a.href = url; a.download = 'safeaiscan-executive-report.pdf'; a.click(); | |
| URL.revokeObjectURL(url); | |
| toast('Executive report downloaded!', 'success'); | |
| } catch(e) { | |
| toast('Executive PDF error: ' + e.message, 'error'); | |
| } | |
| } | |
| /* ββ Keyboard shortcuts βββββββββββββββββββββββββββββββββββββ */ | |
| document.addEventListener('keydown', function(e) { | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { | |
| var panel = document.querySelector('.pro-panel.active'); | |
| if (panel && panel.id === 'panel-code') runCodeScan(); | |
| else if (panel && panel.id === 'panel-ai') runDeepAI(); | |
| else if (panel && panel.id === 'panel-cve') runCVELookup(); | |
| } | |
| if (e.key === 'Escape') closeDrawer(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |