Created
August 27, 2026 09:49
-
-
Save manchumahara/02e5fcf7061675a9804a786e014cc26f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>VPS Security Audit — Dossier</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root{ | |
| --ink:#12141c; | |
| --ink-2:#1b2030; | |
| --paper:#f6f3ea; | |
| --paper-2:#efe9da; | |
| --line:#dad5c6; | |
| --ink-soft:#4b5160; | |
| --ink-faint:#8a8f9c; | |
| --red:#c0272d; | |
| --red-soft:#f5dfdd; | |
| --amber:#b9791a; | |
| --amber-soft:#f6e6cc; | |
| --green:#2e6b45; | |
| --green-soft:#dfe9de; | |
| --gold:#a8842f; | |
| --radius:2px; | |
| font-size:62.5%; | |
| } | |
| *{box-sizing:border-box;} | |
| html,body{margin:0;padding:0;} | |
| body{ | |
| background:var(--ink); | |
| background-image: | |
| radial-gradient(circle at 20% 0%, rgba(168,132,47,0.08), transparent 45%), | |
| radial-gradient(circle at 90% 100%, rgba(46,107,69,0.07), transparent 45%); | |
| color:var(--ink-soft); | |
| font-family:'Inter',sans-serif; | |
| font-size:1.5rem; | |
| line-height:1.5; | |
| min-height:100vh; | |
| } | |
| h1,h2,h3{font-family:'Space Grotesk',sans-serif;margin:0;color:var(--ink);} | |
| .mono{font-family:'JetBrains Mono',monospace;} | |
| a{color:inherit;} | |
| /* ===================== hidden svg filters ===================== */ | |
| .vad-defs{position:absolute;width:0;height:0;overflow:hidden;} | |
| /* ===================== shell ===================== */ | |
| .vad-shell{max-width:1040px;margin:0 auto;padding:4rem 2rem 8rem;} | |
| @media (max-width:640px){ .vad-shell{padding:2.4rem 1.4rem 6rem;} } | |
| /* ===================== loader / intake screen ===================== */ | |
| .vad-intake{ | |
| background:var(--paper); | |
| border:1px solid var(--line); | |
| border-radius:var(--radius); | |
| padding:5rem 4rem; | |
| text-align:center; | |
| box-shadow:0 30px 60px -20px rgba(0,0,0,0.5); | |
| } | |
| @media (max-width:640px){ .vad-intake{padding:3.6rem 2rem;} } | |
| .vad-intake__eyebrow{ | |
| font-family:'JetBrains Mono',monospace; | |
| font-size:1.1rem; | |
| letter-spacing:0.18em; | |
| text-transform:uppercase; | |
| color:var(--gold); | |
| margin-bottom:1.4rem; | |
| } | |
| .vad-intake__title{font-size:3rem;font-weight:700;margin-bottom:1rem;} | |
| .vad-intake__sub{max-width:46rem;margin:0 auto 3.2rem;color:var(--ink-soft);font-size:1.5rem;} | |
| .vad-drop{ | |
| border:2px dashed var(--line); | |
| border-radius:var(--radius); | |
| padding:3.6rem 2rem; | |
| cursor:pointer; | |
| transition:border-color .15s ease, background .15s ease; | |
| background:rgba(255,255,255,0.35); | |
| } | |
| .vad-drop:hover, .vad-drop.is-drag{border-color:var(--gold); background:rgba(168,132,47,0.06);} | |
| .vad-drop__icon{width:4.4rem;height:4.4rem;margin:0 auto 1.4rem;color:var(--ink-faint);} | |
| .vad-drop__label{font-weight:600;color:var(--ink);font-size:1.6rem;margin-bottom:.4rem;} | |
| .vad-drop__hint{font-size:1.3rem;color:var(--ink-faint);} | |
| .vad-intake input[type=file]{display:none;} | |
| .vad-intake__status{ | |
| margin-top:2rem;font-family:'JetBrains Mono',monospace;font-size:1.25rem;color:var(--ink-faint); | |
| min-height:1.8em; | |
| } | |
| .vad-intake__status.is-error{color:var(--red);} | |
| .vad-btn{ | |
| display:inline-block;margin-top:1.6rem;padding:1rem 2rem; | |
| background:var(--ink);color:var(--paper);border:none;border-radius:var(--radius); | |
| font-family:'Space Grotesk',sans-serif;font-weight:600;font-size:1.3rem;letter-spacing:.02em; | |
| cursor:pointer;transition:transform .1s ease, background .15s ease; | |
| } | |
| .vad-btn:hover{background:var(--ink-2);transform:translateY(-1px);} | |
| .vad-btn--ghost{background:transparent;color:var(--ink-soft);border:1px solid var(--line);} | |
| .vad-btn--ghost:hover{background:var(--paper-2);color:var(--ink);} | |
| /* ===================== report ===================== */ | |
| .vad-report{display:none;} | |
| .vad-report.is-active{display:block;} | |
| .vad-intake.is-hidden{display:none;} | |
| .vad-topbar{display:flex;justify-content:space-between;align-items:center;margin-bottom:2.4rem;gap:1rem;flex-wrap:wrap;} | |
| .vad-topbar__brand{font-family:'JetBrains Mono',monospace;font-size:1.15rem;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-faint);} | |
| .vad-topbar__brand strong{color:var(--gold);} | |
| /* header dossier card */ | |
| .vad-header{ | |
| position:relative; | |
| background:var(--paper); | |
| border:1px solid var(--line); | |
| border-radius:var(--radius); | |
| padding:3.4rem; | |
| display:flex;justify-content:space-between;align-items:center;gap:3rem; | |
| overflow:hidden; | |
| box-shadow:0 30px 60px -25px rgba(0,0,0,0.55); | |
| } | |
| @media (max-width:760px){ .vad-header{flex-direction:column;align-items:flex-start;padding:2.4rem;} } | |
| .vad-header::before{ | |
| content:"";position:absolute;inset:8px;border:1px solid rgba(75,81,96,0.18);pointer-events:none; | |
| } | |
| .vad-header__meta{position:relative;z-index:1;} | |
| .vad-header__file{font-family:'JetBrains Mono',monospace;font-size:1.15rem;color:var(--gold);letter-spacing:.08em;text-transform:uppercase;margin-bottom:1rem;} | |
| .vad-header__host{font-size:3.2rem;font-weight:700;color:var(--ink);margin-bottom:.6rem;word-break:break-word;} | |
| .vad-header__sub{font-family:'JetBrains Mono',monospace;font-size:1.3rem;color:var(--ink-soft);} | |
| .vad-header__sub span{color:var(--ink-faint);} | |
| .vad-header__scanned{margin-top:1.6rem;font-size:1.2rem;color:var(--ink-faint);} | |
| /* stamp */ | |
| .vad-stamp{ | |
| position:relative;z-index:1;flex:none; | |
| width:15rem;height:15rem;border-radius:50%; | |
| display:flex;align-items:center;justify-content:center; | |
| transform:rotate(-9deg); | |
| filter:url(#vadRoughen); | |
| } | |
| .vad-stamp::before{ | |
| content:"";position:absolute;inset:0;border-radius:50%; | |
| border:4px solid currentColor; | |
| } | |
| .vad-stamp::after{ | |
| content:"";position:absolute;inset:10px;border-radius:50%; | |
| border:1.5px solid currentColor; | |
| } | |
| .vad-stamp__inner{position:relative;text-align:center;} | |
| .vad-stamp__score{display:block;font-family:'Space Grotesk',sans-serif;font-weight:700;font-size:3rem;line-height:1;} | |
| .vad-stamp__label{display:block;font-family:'JetBrains Mono',monospace;font-size:1.05rem;letter-spacing:.14em;margin-top:.4rem;text-transform:uppercase;} | |
| .vad-stamp--risk{color:var(--red);} | |
| .vad-stamp--attention{color:var(--amber);} | |
| .vad-stamp--secure{color:var(--green);} | |
| /* summary strip */ | |
| .vad-strip{ | |
| display:grid;grid-template-columns:repeat(4,1fr);gap:1px; | |
| background:var(--line); | |
| border:1px solid var(--line); | |
| border-radius:var(--radius); | |
| overflow:hidden; | |
| margin:2rem 0 3.6rem; | |
| } | |
| @media (max-width:640px){ .vad-strip{grid-template-columns:repeat(2,1fr);} } | |
| .vad-strip__cell{background:var(--paper);padding:2rem 1.8rem;} | |
| .vad-strip__num{font-family:'Space Grotesk',sans-serif;font-weight:700;font-size:2.6rem;color:var(--ink);} | |
| .vad-strip__cell--fail .vad-strip__num{color:var(--red);} | |
| .vad-strip__cell--warn .vad-strip__num{color:var(--amber);} | |
| .vad-strip__cell--pass .vad-strip__num{color:var(--green);} | |
| .vad-strip__cap{font-family:'JetBrains Mono',monospace;font-size:1.1rem;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-faint);margin-top:.4rem;} | |
| /* section shell */ | |
| .vad-section{margin-bottom:3.6rem;} | |
| .vad-section__head{display:flex;align-items:baseline;gap:1rem;margin-bottom:1.6rem;flex-wrap:wrap;} | |
| .vad-section__title{font-size:1.9rem;font-weight:700;color:var(--paper);} | |
| .vad-section__count{font-family:'JetBrains Mono',monospace;font-size:1.2rem;color:var(--ink-faint);} | |
| .vad-section__rule{flex:1;height:1px;background:rgba(246,243,234,0.14);min-width:2rem;} | |
| /* system spec grid */ | |
| .vad-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1px;background:var(--line);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;} | |
| @media (max-width:760px){ .vad-grid{grid-template-columns:repeat(2,1fr);} } | |
| .vad-grid__cell{background:var(--paper);padding:1.8rem;} | |
| .vad-grid__key{font-family:'JetBrains Mono',monospace;font-size:1.05rem;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-faint);margin-bottom:.5rem;} | |
| .vad-grid__val{font-size:1.4rem;color:var(--ink);font-weight:600;word-break:break-word;} | |
| .vad-grid__val.mono{font-size:1.3rem;font-weight:500;} | |
| /* findings */ | |
| .vad-filters{display:flex;gap:.8rem;margin-bottom:1.6rem;flex-wrap:wrap;} | |
| .vad-chip{ | |
| font-family:'JetBrains Mono',monospace;font-size:1.1rem;letter-spacing:.06em;text-transform:uppercase; | |
| padding:.7rem 1.4rem;border-radius:100px;border:1px solid rgba(246,243,234,0.25); | |
| background:transparent;color:var(--paper);cursor:pointer;transition:all .12s ease; | |
| } | |
| .vad-chip.is-active{background:var(--paper);color:var(--ink);border-color:var(--paper);} | |
| .vad-chip:hover:not(.is-active){border-color:var(--gold);color:var(--gold);} | |
| .vad-findings{display:flex;flex-direction:column;gap:1px;background:var(--line);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;} | |
| .vad-finding{background:var(--paper);padding:1.6rem 1.8rem;display:flex;gap:1.6rem;align-items:flex-start;} | |
| @media (max-width:600px){ .vad-finding{flex-direction:column;gap:.8rem;} } | |
| .vad-badge{ | |
| flex:none;font-family:'JetBrains Mono',monospace;font-weight:700;font-size:1.05rem;letter-spacing:.08em; | |
| padding:.45rem 1rem;border-radius:2px;transform:rotate(-3deg); | |
| border:2px solid currentColor; | |
| } | |
| .vad-badge--fail{color:var(--red);} | |
| .vad-badge--warn{color:var(--amber);} | |
| .vad-badge--pass{color:var(--green);} | |
| .vad-finding__body{flex:1;min-width:0;} | |
| .vad-finding__title{font-weight:700;color:var(--ink);font-size:1.5rem;margin-bottom:.3rem;} | |
| .vad-finding__desc{color:var(--ink-soft);font-size:1.35rem;} | |
| /* ports */ | |
| .vad-portsmeta{font-family:'JetBrains Mono',monospace;font-size:1.2rem;color:var(--ink-faint);margin-bottom:1.4rem;} | |
| .vad-ports{display:flex;flex-wrap:wrap;gap:.8rem;} | |
| .vad-port{ | |
| font-family:'JetBrains Mono',monospace;font-size:1.25rem;font-weight:700; | |
| padding:.8rem 1.2rem;border-radius:2px;background:var(--paper);border:1px solid var(--line); | |
| display:flex;flex-direction:column;align-items:flex-start;min-width:6.4rem; | |
| } | |
| .vad-port__num{color:var(--ink);} | |
| .vad-port__tag{font-size:.95rem;font-weight:500;letter-spacing:.04em;text-transform:uppercase;margin-top:.2rem;} | |
| .vad-port--risk{border-color:var(--red);} .vad-port--risk .vad-port__tag{color:var(--red);} | |
| .vad-port--caution{border-color:var(--amber);} .vad-port--caution .vad-port__tag{color:var(--amber);} | |
| .vad-port--ok{border-color:var(--green);} .vad-port--ok .vad-port__tag{color:var(--green);} | |
| .vad-legend{display:flex;gap:1.8rem;margin-top:1.6rem;flex-wrap:wrap;font-family:'JetBrains Mono',monospace;font-size:1.1rem;color:var(--ink-faint);} | |
| .vad-legend span{display:inline-flex;align-items:center;gap:.6rem;} | |
| .vad-legend i{width:1rem;height:1rem;border-radius:2px;display:inline-block;} | |
| /* footer */ | |
| .vad-footer{ | |
| margin-top:4rem;padding-top:2.4rem;border-top:1px solid rgba(246,243,234,0.14); | |
| display:flex;justify-content:space-between;align-items:center;gap:1.4rem;flex-wrap:wrap; | |
| font-family:'JetBrains Mono',monospace;font-size:1.15rem;color:var(--ink-faint); | |
| } | |
| .vad-footer__note{max-width:60rem;font-family:'Inter',sans-serif;font-size:1.3rem;line-height:1.6;} | |
| @media (prefers-reduced-motion: reduce){ | |
| .vad-btn,.vad-drop,.vad-chip{transition:none;} | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <svg class="vad-defs" aria-hidden="true"> | |
| <filter id="vadRoughen" x="-20%" y="-20%" width="140%" height="140%"> | |
| <feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="7" result="noise"/> | |
| <feDisplacementMap in="SourceGraphic" in2="noise" scale="3.2"/> | |
| </filter> | |
| </svg> | |
| <div class="vad-shell"> | |
| <!-- ============== INTAKE SCREEN ============== --> | |
| <section class="vad-intake" id="vadIntake"> | |
| <div class="vad-intake__eyebrow">File No. — VPS/SEC/AUDIT</div> | |
| <h1 class="vad-intake__title">Load a VPS security audit</h1> | |
| <p class="vad-intake__sub">Drop the raw <span class="mono">vps-audit-report.txt</span> output here, or browse for a file. If a file named <span class="mono">vps-audit-report.txt</span> sits next to this page and it's served over HTTP, it loads automatically.</p> | |
| <label class="vad-drop" id="vadDrop" for="vadFileInput"> | |
| <svg class="vad-drop__icon" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.6"> | |
| <path d="M24 6v24m0 0-8-8m8 8 8-8" stroke-linecap="round" stroke-linejoin="round"/> | |
| <path d="M8 32v6a4 4 0 0 0 4 4h24a4 4 0 0 0 4-4v-6" stroke-linecap="round" stroke-linejoin="round"/> | |
| </svg> | |
| <div class="vad-drop__label">Drop report .txt here, or click to browse</div> | |
| <div class="vad-drop__hint">Parsed entirely in your browser — nothing is uploaded anywhere</div> | |
| </label> | |
| <input type="file" id="vadFileInput" accept=".txt,text/plain"> | |
| <div><button class="vad-btn vad-btn--ghost" id="vadRetryDefault" type="button">Look for vps-audit-report.txt again</button></div> | |
| <div class="vad-intake__status" id="vadStatus">Checking for vps-audit-report.txt in this folder…</div> | |
| </section> | |
| <!-- ============== REPORT SCREEN ============== --> | |
| <section class="vad-report" id="vadReport"> | |
| <div class="vad-topbar"> | |
| <div class="vad-topbar__brand">VPS Security Audit <strong>Dossier</strong></div> | |
| <button class="vad-btn vad-btn--ghost" id="vadLoadAnother" type="button">Load a different report</button> | |
| </div> | |
| <div class="vad-header"> | |
| <div class="vad-header__meta"> | |
| <div class="vad-header__file">Classification — Security Audit</div> | |
| <div class="vad-header__host" id="hHost">—</div> | |
| <div class="vad-header__sub"><span>OS</span> <span id="hOs" class="mono">—</span> · <span>IP</span> <span id="hIp" class="mono">—</span></div> | |
| <div class="vad-header__scanned mono" id="hScanned">—</div> | |
| </div> | |
| <div class="vad-stamp" id="hStamp"> | |
| <div class="vad-stamp__inner"> | |
| <span class="vad-stamp__score" id="hScore">--</span> | |
| <span class="vad-stamp__label" id="hVerdict">—</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="vad-strip"> | |
| <div class="vad-strip__cell vad-strip__cell--fail"><div class="vad-strip__num" id="cFail">0</div><div class="vad-strip__cap">Failed checks</div></div> | |
| <div class="vad-strip__cell vad-strip__cell--warn"><div class="vad-strip__num" id="cWarn">0</div><div class="vad-strip__cap">Warnings</div></div> | |
| <div class="vad-strip__cell vad-strip__cell--pass"><div class="vad-strip__num" id="cPass">0</div><div class="vad-strip__cap">Passed checks</div></div> | |
| <div class="vad-strip__cell"><div class="vad-strip__num" id="cTotal">0</div><div class="vad-strip__cap">Total checks</div></div> | |
| </div> | |
| <div class="vad-section"> | |
| <div class="vad-section__head"><h2 class="vad-section__title">System spec sheet</h2><div class="vad-section__rule"></div></div> | |
| <div class="vad-grid" id="sysGrid"></div> | |
| </div> | |
| <div class="vad-section"> | |
| <div class="vad-section__head"> | |
| <h2 class="vad-section__title">Findings</h2> | |
| <span class="vad-section__count" id="findCount"></span> | |
| <div class="vad-section__rule"></div> | |
| </div> | |
| <div class="vad-filters" id="vadFilters"> | |
| <button class="vad-chip is-active" data-filter="all" type="button">All</button> | |
| <button class="vad-chip" data-filter="fail" type="button">Fail</button> | |
| <button class="vad-chip" data-filter="warn" type="button">Warn</button> | |
| <button class="vad-chip" data-filter="pass" type="button">Pass</button> | |
| </div> | |
| <div class="vad-findings" id="findingsList"></div> | |
| </div> | |
| <div class="vad-section" id="portsSection"> | |
| <div class="vad-section__head"><h2 class="vad-section__title">Port exposure</h2><div class="vad-section__rule"></div></div> | |
| <div class="vad-portsmeta" id="portsMeta"></div> | |
| <div class="vad-ports" id="portsList"></div> | |
| <div class="vad-legend"> | |
| <span><i style="background:var(--green)"></i>Expected / low risk</span> | |
| <span><i style="background:var(--amber)"></i>Harden or verify</span> | |
| <span><i style="background:var(--red)"></i>Publicly exposed — investigate</span> | |
| </div> | |
| </div> | |
| <div class="vad-footer"> | |
| <div class="vad-footer__note" id="footNote">Review every failed check first, then warnings. Passed checks need no action.</div> | |
| <div id="footTool">—</div> | |
| </div> | |
| </section> | |
| </div> | |
| <script> | |
| (function(){ | |
| "use strict"; | |
| var RISK_PORTS = { | |
| "3306":"MySQL","5432":"PostgreSQL","6379":"Redis","11211":"Memcached","27017":"MongoDB", | |
| "5672":"RabbitMQ","15672":"RabbitMQ UI","25672":"RabbitMQ cluster","4369":"EPMD", | |
| "9000":"App/console","8080":"HTTP-alt","3001":"App","7000":"App","7500":"App", | |
| "8025":"Mail UI","1025":"SMTP test","631":"CUPS printing" | |
| }; | |
| var CAUTION_PORTS = { "22":"SSH", "53":"DNS" }; | |
| var OK_PORTS = { "80":"HTTP", "443":"HTTPS", "51820":"WireGuard VPN" }; | |
| function classifyPort(p){ | |
| if (RISK_PORTS[p]) return { level:"risk", tag:RISK_PORTS[p] }; | |
| if (CAUTION_PORTS[p]) return { level:"caution", tag:CAUTION_PORTS[p] }; | |
| if (OK_PORTS[p]) return { level:"ok", tag:OK_PORTS[p] }; | |
| return { level:"caution", tag:"Unknown" }; | |
| } | |
| function escapeHtml(s){ | |
| return String(s).replace(/[&<>"']/g, function(c){ | |
| return { "&":"&", "<":"<", ">":">", '"':""", "'":"'" }[c]; | |
| }); | |
| } | |
| function parseReport(text){ | |
| var lines = text.replace(/\r/g, "").split("\n"); | |
| var data = { | |
| tool:"", url:"", scannedAt:"", | |
| system:{}, findings:[], ports:[], portsTotal:null, portsPublic:null, | |
| notes:[] | |
| }; | |
| var section = "head"; // head | system | security | summary | |
| var findRe = /^\[(PASS|WARN|FAIL)\]\s*([^-]+?)\s*-\s*(.*)$/; | |
| var kvRe = /^([A-Za-z][A-Za-z0-9 /()]+?):\s*(.+)$/; | |
| var portsRe = /Total:\s*(\d+),\s*Public:\s*(\d+)\s*accessible ports\)?:?\s*([\d,\s]+)/i; | |
| for (var i = 0; i < lines.length; i++){ | |
| var raw = lines[i]; | |
| var line = raw.trim(); | |
| if (!line) continue; | |
| if (/^=+$/.test(line)) continue; | |
| if (/^security audit results$/i.test(line)) { section = "security"; continue; } | |
| if (/^system information summary$/i.test(line)) { section = "summary"; continue; } | |
| if (/^system information$/i.test(line)) { section = "system"; continue; } | |
| if (/^end of vps audit report$/i.test(line)) { section = "end"; continue; } | |
| if (section === "head"){ | |
| if (/^https?:\/\//i.test(line)) { data.url = line; continue; } | |
| var startedMatch = line.match(/^Starting audit at (.+)$/i); | |
| if (startedMatch) { data.scannedAt = startedMatch[1]; continue; } | |
| if (!data.tool && /audit tool/i.test(line)) { data.tool = line; continue; } | |
| if (!data.tool) { data.tool = line; continue; } | |
| continue; | |
| } | |
| var fm = line.match(findRe); | |
| if (fm){ | |
| data.findings.push({ status: fm[1], title: fm[2].trim(), desc: fm[3].trim() }); | |
| if (/port security/i.test(fm[2])){ | |
| var pm = fm[3].match(portsRe); | |
| if (pm){ | |
| data.portsTotal = pm[1]; | |
| data.portsPublic = pm[2]; | |
| data.ports = pm[3].split(",").map(function(p){ return p.trim(); }).filter(Boolean); | |
| } | |
| } | |
| continue; | |
| } | |
| if (section === "end"){ | |
| data.notes.push(line); | |
| continue; | |
| } | |
| var kv = line.match(kvRe); | |
| if (kv && (section === "system" || section === "summary" || section === "security")){ | |
| var key = kv[1].trim(); | |
| var val = kv[2].trim(); | |
| if (section === "security"){ | |
| data.notes.push(key + ": " + val); | |
| } else { | |
| data.system[key] = val; | |
| } | |
| continue; | |
| } | |
| } | |
| return data; | |
| } | |
| function scoreReport(findings){ | |
| var counts = { PASS:0, WARN:0, FAIL:0 }; | |
| findings.forEach(function(f){ counts[f.status] = (counts[f.status]||0) + 1; }); | |
| var total = findings.length || 1; | |
| var weighted = counts.PASS * 1 + counts.WARN * 0.5 + counts.FAIL * 0; | |
| var score = Math.round((weighted / total) * 100); | |
| var verdict, tone; | |
| if (score >= 85) { verdict = "Secure"; tone = "secure"; } | |
| else if (score >= 55) { verdict = "Needs attention"; tone = "attention"; } | |
| else { verdict = "At risk"; tone = "risk"; } | |
| return { counts: counts, total: findings.length, score: score, verdict: verdict, tone: tone }; | |
| } | |
| function renderReport(data){ | |
| var summary = scoreReport(data.findings); | |
| document.getElementById("hHost").textContent = data.system["Hostname"] || "Unknown host"; | |
| document.getElementById("hOs").textContent = data.system["Operating System"] || "—"; | |
| document.getElementById("hIp").textContent = data.system["Public IP"] || "—"; | |
| document.getElementById("hScanned").textContent = data.scannedAt ? ("Scanned " + data.scannedAt) : ""; | |
| var stampEl = document.getElementById("hStamp"); | |
| stampEl.className = "vad-stamp vad-stamp--" + summary.tone; | |
| document.getElementById("hScore").textContent = summary.score; | |
| document.getElementById("hVerdict").textContent = summary.verdict; | |
| document.getElementById("cFail").textContent = summary.counts.FAIL || 0; | |
| document.getElementById("cWarn").textContent = summary.counts.WARN || 0; | |
| document.getElementById("cPass").textContent = summary.counts.PASS || 0; | |
| document.getElementById("cTotal").textContent = summary.total; | |
| // system spec grid — curated key order, then whatever else exists | |
| var order = ["Hostname","Operating System","Kernel Version","Uptime","CPU Model","CPU Cores","Total Memory","Total Disk Space","Public IP","Load Average"]; | |
| var grid = document.getElementById("sysGrid"); | |
| grid.innerHTML = ""; | |
| var shown = {}; | |
| order.concat(Object.keys(data.system)).forEach(function(key){ | |
| if (shown[key] || !data.system[key]) return; | |
| shown[key] = true; | |
| var cell = document.createElement("div"); | |
| cell.className = "vad-grid__cell"; | |
| cell.innerHTML = '<div class="vad-grid__key">' + escapeHtml(key) + '</div>' + | |
| '<div class="vad-grid__val mono">' + escapeHtml(data.system[key]) + '</div>'; | |
| grid.appendChild(cell); | |
| }); | |
| // findings | |
| document.getElementById("findCount").textContent = summary.total + " checks"; | |
| var list = document.getElementById("findingsList"); | |
| var order2 = { FAIL:0, WARN:1, PASS:2 }; | |
| var sorted = data.findings.slice().sort(function(a,b){ return order2[a.status]-order2[b.status]; }); | |
| function paint(filter){ | |
| list.innerHTML = ""; | |
| sorted.filter(function(f){ return filter === "all" || f.status.toLowerCase() === filter; }) | |
| .forEach(function(f){ | |
| var row = document.createElement("div"); | |
| row.className = "vad-finding"; | |
| row.innerHTML = | |
| '<span class="vad-badge vad-badge--' + f.status.toLowerCase() + '">' + f.status + '</span>' + | |
| '<div class="vad-finding__body">' + | |
| '<div class="vad-finding__title">' + escapeHtml(f.title) + '</div>' + | |
| '<div class="vad-finding__desc">' + escapeHtml(f.desc) + '</div>' + | |
| '</div>'; | |
| list.appendChild(row); | |
| }); | |
| } | |
| paint("all"); | |
| Array.prototype.forEach.call(document.querySelectorAll(".vad-chip"), function(chip){ | |
| chip.addEventListener("click", function(){ | |
| Array.prototype.forEach.call(document.querySelectorAll(".vad-chip"), function(c){ c.classList.remove("is-active"); }); | |
| chip.classList.add("is-active"); | |
| paint(chip.getAttribute("data-filter")); | |
| }); | |
| }); | |
| // ports | |
| var portsSection = document.getElementById("portsSection"); | |
| if (data.ports.length){ | |
| portsSection.style.display = ""; | |
| document.getElementById("portsMeta").textContent = | |
| (data.portsTotal ? data.portsTotal + " open ports total" : data.ports.length + " open ports") + | |
| (data.portsPublic ? " · " + data.portsPublic + " reachable from the public internet" : ""); | |
| var pl = document.getElementById("portsList"); | |
| pl.innerHTML = ""; | |
| data.ports.forEach(function(p){ | |
| var c = classifyPort(p); | |
| var el = document.createElement("div"); | |
| el.className = "vad-port vad-port--" + c.level; | |
| el.innerHTML = '<span class="vad-port__num">' + p + '</span><span class="vad-port__tag">' + c.tag + '</span>'; | |
| pl.appendChild(el); | |
| }); | |
| } else { | |
| portsSection.style.display = "none"; | |
| } | |
| document.getElementById("footTool").textContent = data.tool || ""; | |
| if (data.notes.length){ | |
| document.getElementById("footNote").textContent = data.notes[data.notes.length-1]; | |
| } | |
| document.getElementById("vadIntake").classList.add("is-hidden"); | |
| document.getElementById("vadReport").classList.add("is-active"); | |
| } | |
| function loadText(text){ | |
| try { | |
| var data = parseReport(text); | |
| if (!data.findings.length){ | |
| setStatus("That file didn't look like a VPS audit report — no [PASS]/[WARN]/[FAIL] lines found.", true); | |
| return; | |
| } | |
| renderReport(data); | |
| } catch (e){ | |
| setStatus("Could not parse that file: " + e.message, true); | |
| } | |
| } | |
| function setStatus(msg, isError){ | |
| var el = document.getElementById("vadStatus"); | |
| el.textContent = msg; | |
| el.classList.toggle("is-error", !!isError); | |
| } | |
| function tryLoadDefault(){ | |
| setStatus("Checking for vps-audit-report.txt in this folder…", false); | |
| fetch("vps-audit-report.txt", { cache: "no-store" }) | |
| .then(function(res){ | |
| if (!res.ok) throw new Error("not found"); | |
| return res.text(); | |
| }) | |
| .then(function(text){ loadText(text); }) | |
| .catch(function(){ | |
| setStatus("No vps-audit-report.txt found here (or the page was opened directly as a file — browsers block that lookup over file://). Drop a file below instead.", false); | |
| }); | |
| } | |
| // file input / drag & drop | |
| var fileInput = document.getElementById("vadFileInput"); | |
| var drop = document.getElementById("vadDrop"); | |
| fileInput.addEventListener("change", function(e){ | |
| var file = e.target.files[0]; | |
| if (!file) return; | |
| var reader = new FileReader(); | |
| reader.onload = function(ev){ loadText(ev.target.result); }; | |
| reader.readAsText(file); | |
| }); | |
| ["dragenter","dragover"].forEach(function(evt){ | |
| drop.addEventListener(evt, function(e){ e.preventDefault(); drop.classList.add("is-drag"); }); | |
| }); | |
| ["dragleave","drop"].forEach(function(evt){ | |
| drop.addEventListener(evt, function(e){ e.preventDefault(); drop.classList.remove("is-drag"); }); | |
| }); | |
| drop.addEventListener("drop", function(e){ | |
| var file = e.dataTransfer.files[0]; | |
| if (!file) return; | |
| var reader = new FileReader(); | |
| reader.onload = function(ev){ loadText(ev.target.result); }; | |
| reader.readAsText(file); | |
| }); | |
| document.getElementById("vadRetryDefault").addEventListener("click", tryLoadDefault); | |
| document.getElementById("vadLoadAnother").addEventListener("click", function(){ | |
| document.getElementById("vadReport").classList.remove("is-active"); | |
| document.getElementById("vadIntake").classList.remove("is-hidden"); | |
| tryLoadDefault(); | |
| }); | |
| tryLoadDefault(); | |
| })(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment