';
// FIX: Category health bars - clean implementation
var catBars="";
CATS.forEach(function(cat){
var it=allData[cat.id]||[];if(!it.length)return;
var actionable=it.filter(function(r){return r.status!=="info";});
var f=it.filter(function(r){return r.status==="fail";}).length;
var w=it.filter(function(r){return r.status==="warn";}).length;
var p=it.filter(function(r){return r.status==="pass";}).length;
var isInfoOnly=actionable.length===0;
var cannotTest=!isInfoOnly&&actionable.every(function(r){
var d=(r.detail||"").toLowerCase();
return d.indexOf("proxy")>=0||d.indexOf("requires")>=0||d.indexOf("run:")>=0||d.indexOf("dig ")>=0||d.indexOf("manual")>=0||d.indexOf("skipped")>=0;
});
var hasRealResult=actionable.some(function(r){return(r.status==="fail"||r.status==="pass")&&(r.detail||"").toLowerCase().indexOf("proxy")<0;});
if((isInfoOnly||cannotTest)&&!hasRealResult){
catBars+='
'+
'
'+cat.ico+' '+cat.name+'
'+
'
'+
'
N/A
';
return;
}
var passScore=p+(w*0.5);
var pct=actionable.length>0?Math.round((passScore/actionable.length)*100):0;
var col=f>0?"#ff4060":w>0?"#f59e0b":"#00e5a0";
catBars+='
'+
'
'+cat.ico+' '+cat.name+'
'+
'
'+
'
'+pct+'%
';
});
var tops=allR.filter(function(r){return r.sev==="critical"||r.sev==="high"||r.status==="fail";}).slice(0,8);
var findHTML=tops.length?tops.map(function(r){
var bc=SC[r.sev]||"#6b7fa8",cn="";
CATS.forEach(function(cat){if((allData[cat.id]||[]).indexOf(r)!==-1)cn=cat.name;});
return'
';
// Radar chart
var rcats=CATS.slice(0,8),cx=110,cy=110,rad=80;
var ptData=rcats.map(function(cat,i){
var angle=(i/rcats.length)*Math.PI*2-Math.PI/2;
var it=allData[cat.id]||[];
var actionable=it.filter(function(r){return r.status!=="info";});
var p2=it.filter(function(r){return r.status==="pass";}).length;
var f2=it.filter(function(r){return r.status==="fail";}).length;
var w2=it.filter(function(r){return r.status==="warn";}).length;
var score2=actionable.length===0?0.6:Math.max(0.08,(p2-(f2*0.8)-(w2*0.3))/Math.max(actionable.length,1));
var rv=rad*Math.min(1,Math.max(0.08,score2));
return{x:(cx+Math.cos(angle)*rv).toFixed(1),y:(cy+Math.sin(angle)*rv).toFixed(1),
lx:(cx+Math.cos(angle)*(rad+22)).toFixed(1),ly:(cy+Math.sin(angle)*(rad+22)).toFixed(1),name:cat.name.split(" ")[0]};
});
var gridPts=rcats.map(function(_,i){var a=(i/rcats.length)*Math.PI*2-Math.PI/2;return{x:(cx+Math.cos(a)*rad).toFixed(1),y:(cy+Math.sin(a)*rad).toFixed(1)};});
var svg='';
dv.innerHTML=
'
';
}
function renderHistList(filter){
var list=document.getElementById("histList");
var filtered=filter?hist.filter(function(h){return h.url.toLowerCase().indexOf(filter.toLowerCase())>=0;}):hist;
if(!filtered.length){list.innerHTML='
No scans yet
';return;}
// Group by domain for timeline
var byDomain={};
filtered.forEach(function(h){
var dom="";try{dom=new URL(h.url).hostname;}catch(e){dom=h.url;}
if(!byDomain[dom])byDomain[dom]=[];
byDomain[dom].push(h);
});
var html="";
Object.entries(byDomain).forEach(function(entry){
var dom=entry[0],scans=entry[1];
// Sort oldest→newest for timeline
var sorted=scans.slice().sort(function(a,b){return a.time-b.time;});
var latest=scans[0]; // hist is newest-first
var gc=latest.score>=80?"#00e5a0":latest.score>=60?"#f59e0b":"#ff4060";
// Draw inline SVG timeline if >1 scan
var timelineSvg="";
if(sorted.length>1){
var W=180,H=36,pad=8;
var scores=sorted.map(function(s){return s.score;});
var minS=Math.max(0,Math.min.apply(null,scores)-10);
var maxS=Math.min(100,Math.max.apply(null,scores)+10);
var pts=scores.map(function(s,i){
var x=pad+(i/(scores.length-1))*(W-pad*2);
var y=H-pad-((s-minS)/(maxS-minS||1))*(H-pad*2);
return{x:x.toFixed(1),y:y.toFixed(1),s:s};
});
var polyline=pts.map(function(p){return p.x+","+p.y;}).join(" ");
// Fill area under line
var fillPts=""+pad+","+(H-pad)+" "+polyline+" "+(W-pad)+","+(H-pad);
var trend=scores[scores.length-1]-scores[0];
var lineColor=trend>0?"#00e5a0":trend<0?"#ff4060":"#f59e0b";
timelineSvg='';
}
// Domain header
html+='
';
// Timeline chart row (if multiple scans)
if(sorted.length>1){
var trend=sorted[sorted.length-1].score-sorted[0].score;
var trendCol=trend>0?"#00e5a0":trend<0?"#ff4060":"#f59e0b";
var trendStr=(trend>0?"+":"")+trend+" pts";
html+='
'+
timelineSvg+
'
'+
'
Trend over '+sorted.length+' scans
'+
'
'+trendStr+'
'+
'
'+
sorted.map(function(s){
var c=s.score>=80?"#00e5a0":s.score>=60?"#f59e0b":"#ff4060";
return''+s.score+'';
}).join(' → ')+
'
'+
'
'+
'
';
}
// Individual scan items
scans.forEach(function(h){
var gc2=h.score>=80?"#00e5a0":h.score>=60?"#f59e0b":"#ff4060";
var d=Math.round((Date.now()-h.time)/60000);
var rel=d<1?"just now":d<60?d+"m ago":d<1440?Math.round(d/60)+"h ago":Math.round(d/1440)+"d ago";
var realIdx=hist.indexOf(h);
html+='
';
});
});
list.innerHTML=html;
}
function deleteHistItem(i){hist.splice(i,1);saveHist();renderHistList(document.getElementById("histSearch").value);toast("Deleted","success",1500);}
document.getElementById("histBtn").onclick=function(){showView("histPanel");renderHistList("");};
document.getElementById("histCloseBtn").onclick=function(){showHome();};
document.getElementById("histClearBtn").onclick=function(){if(!confirm("Clear all history?"))return;hist=[];saveHist();renderHistList("");toast("Cleared");};
document.getElementById("histSearch").oninput=function(){renderHistList(this.value);};
document.getElementById("cmpBtn").onclick=function(){
showView("cmpView");
var cv=document.getElementById("cmpView");
if(hist.length<2){
cv.innerHTML='
Compare
Run at least 2 scans to compare.
';
return;
}
var opts=hist.map(function(h,i){
var dom="";try{dom=new URL(h.url).hostname;}catch(e){dom=h.url;}
var d=Math.round((Date.now()-h.time)/60000);
var rel=d<1?"just now":d<60?d+"m ago":d<1440?Math.round(d/60)+"h ago":Math.round(d/1440)+"d ago";
return'';
}).join("");
cv.innerHTML=
'
Scan Diff
'+
'
'+
'
Scan A (older)
'+
'
'+
'
→
'+
'
Scan B (newer)
'+
'
'+
'
'+
'';
// default: first vs second
if(hist.length>=2){
document.getElementById("cmpA").value="1";
document.getElementById("cmpB").value="0";
}
renderCmp();
};
function renderCmp(){
var aIdx=parseInt(document.getElementById("cmpA").value||"0");
var bIdx=document.getElementById("cmpB").value;
if(bIdx==="")return;
bIdx=parseInt(bIdx);
if(isNaN(bIdx)||aIdx===bIdx){
document.getElementById("cmpResult").innerHTML='
Select two different scans.
';
return;
}
var a=hist[aIdx],b=hist[bIdx];
var diff=b.score-a.score;
var diffCol=diff>0?"#00e5a0":diff<0?"#ff4060":"#6b7fa8";
var gA=a.score>=80?"#00e5a0":a.score>=60?"#f59e0b":"#ff4060";
var gB=b.score>=80?"#00e5a0":b.score>=60?"#f59e0b":"#ff4060";
var domA="";try{domA=new URL(a.url).hostname;}catch(e){domA=a.url;}
var domB="";try{domB=new URL(b.url).hostname;}catch(e){domB=b.url;}
var dA=new Date(a.time).toLocaleString();
var dB=new Date(b.time).toLocaleString();
// ── Score header ──
var header=
'
';
}
// ── Fix snippets database ──
var FIX_DB={
// HTTP Headers
"csp":{
title:"Content-Security-Policy",
iis:'',
nginx:"add_header Content-Security-Policy \"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-ancestors 'none';\" always;",
apache:'Header always set Content-Security-Policy "default-src \'self\'; script-src \'self\' \'unsafe-inline\'; img-src \'self\' data: https:; frame-ancestors \'none\';"',
node:"app.use(helmet.contentSecurityPolicy({ directives: { defaultSrc: [\"'self'\"], scriptSrc: [\"'self'\", \"'unsafe-inline'\"], imgSrc: [\"'self'\", \"data:\", \"https:\"], frameAncestors: [\"'none'\"] } }));",
},
"hsts":{
title:"Strict-Transport-Security",
iis:'',
nginx:"add_header Strict-Transport-Security \"max-age=31536000; includeSubDomains; preload\" always;",
apache:'Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"',
node:'app.use(helmet.hsts({ maxAge: 31536000, includeSubDomains: true, preload: true }));',
},
"xfo":{
title:"X-Frame-Options",
iis:'',
nginx:"add_header X-Frame-Options \"DENY\" always;",
apache:'Header always set X-Frame-Options "DENY"',
node:'app.use(helmet.frameguard({ action: "deny" }));',
},
"rp":{
title:"Referrer-Policy",
iis:'',
nginx:'add_header Referrer-Policy "strict-origin-when-cross-origin" always;',
apache:'Header always set Referrer-Policy "strict-origin-when-cross-origin"',
node:'app.use(helmet.referrerPolicy({ policy: "strict-origin-when-cross-origin" }));',
},
"pp":{
title:"Permissions-Policy",
iis:'',
nginx:'add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;',
apache:'Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"',
node:'app.use(helmet.permittedCrossDomainPolicies()); // also add manually: res.setHeader("Permissions-Policy", "camera=(), microphone=()")',
},
"xcto":{
title:"X-Content-Type-Options",
iis:'',
nginx:'add_header X-Content-Type-Options "nosniff" always;',
apache:'Header always set X-Content-Type-Options "nosniff"',
node:'app.use(helmet.noSniff());',
},
"coep":{
title:"Cross-Origin-Embedder-Policy",
iis:'',
nginx:'add_header Cross-Origin-Embedder-Policy "require-corp" always;',
apache:'Header always set Cross-Origin-Embedder-Policy "require-corp"',
node:'res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");',
},
"coop":{
title:"Cross-Origin-Opener-Policy",
iis:'',
nginx:'add_header Cross-Origin-Opener-Policy "same-origin" always;',
apache:'Header always set Cross-Origin-Opener-Policy "same-origin"',
node:'res.setHeader("Cross-Origin-Opener-Policy", "same-origin");',
},
"corp":{
title:"Cross-Origin-Resource-Policy",
iis:'',
nginx:'add_header Cross-Origin-Resource-Policy "same-origin" always;',
apache:'Header always set Cross-Origin-Resource-Policy "same-origin"',
node:'res.setHeader("Cross-Origin-Resource-Policy", "same-origin");',
},
"cc":{
title:"Cache-Control",
iis:'',
nginx:'add_header Cache-Control "no-store, no-cache, must-revalidate" always;',
apache:'Header always set Cache-Control "no-store, no-cache, must-revalidate"',
node:'res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");',
},
// Security issues
"open_redirect":{
title:"Open Redirect Fix",
iis:"// In your controller/handler:\nstring[] allowedHosts = { \"yoursite.com\", \"www.yoursite.com\" };\nif (!allowedHosts.Contains(new Uri(returnUrl).Host))\n return Redirect(\"/\");",
nginx:"# Block common redirect params at Nginx level:\nif ($arg_redirect ~* \"^https?://(?!yoursite\\.com)\") { return 403; }\nif ($arg_url ~* \"^https?://(?!yoursite\\.com)\") { return 403; }",
apache:"RewriteCond %{QUERY_STRING} (redirect|url|next|return)=https?://(?!yoursite\\.com) [NC]\nRewriteRule .* - [F,L]",
node:"const allowedHosts = ['yoursite.com'];\nconst returnUrl = req.query.redirect || req.query.url;\nif (returnUrl) {\n const host = new URL(returnUrl).hostname;\n if (!allowedHosts.includes(host)) return res.redirect('/');\n}",
},
"csrf_missing":{
title:"CSRF Protection",
iis:"// ASP.NET MVC — add to controller:\n[ValidateAntiForgeryToken]\npublic ActionResult Submit(FormModel model) { ... }\n// In view:\n@Html.AntiForgeryToken()",
nginx:"# CSRF must be handled in application code, not Nginx.\n# Add SameSite cookie attribute:\nproxy_cookie_flags ~ secure httponly samesite=strict;",
apache:"# In PHP:\nsession_start();\nif (empty($_SESSION['csrf_token'])) {\n $_SESSION['csrf_token'] = bin2hex(random_bytes(32));\n}\n// Validate on POST:\nif ($_POST['csrf_token'] !== $_SESSION['csrf_token']) die('Invalid CSRF token');",
node:"const csrf = require('csurf');\napp.use(csrf({ cookie: { httpOnly: true, secure: true, sameSite: 'strict' } }));\napp.get('/form', (req, res) => res.render('form', { csrfToken: req.csrfToken() }));",
},
"cookie_secure":{
title:"Cookie Secure + HttpOnly + SameSite",
iis:"\n",
nginx:"proxy_cookie_flags ~ secure httponly samesite=strict;",
apache:"Header always edit Set-Cookie ^(.*)$ \"$1; Secure; HttpOnly; SameSite=Strict\"",
node:"app.use(session({\n cookie: {\n secure: true,\n httpOnly: true,\n sameSite: 'strict',\n maxAge: 3600000\n }\n}));",
},
"cors":{
title:"CORS Fix (Wildcard)",
iis:'\n',
nginx:"# Replace wildcard with specific origin:\nadd_header Access-Control-Allow-Origin \"https://yoursite.com\" always;\n# Or use map for multiple origins:\nmap $http_origin $cors_origin {\n default \"\";\n \"https://yoursite.com\" $http_origin;\n \"https://app.yoursite.com\" $http_origin;\n}\nadd_header Access-Control-Allow-Origin $cors_origin always;",
apache:'Header always set Access-Control-Allow-Origin "https://yoursite.com"',
node:"const allowedOrigins = ['https://yoursite.com', 'https://app.yoursite.com'];\napp.use(cors({ origin: (origin, cb) => allowedOrigins.includes(origin) ? cb(null, true) : cb(new Error('Not allowed')) }));",
},
"rate_limiting":{
title:"Rate Limiting",
iis:"\n\n \n \n",
nginx:"limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;\nlimit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;\nlocation /api/ { limit_req zone=api burst=40 nodelay; limit_req_status 429; }\nlocation /login { limit_req zone=login burst=3 nodelay; }",
apache:"# Install mod_evasive:\n\n DOSHashTableSize 3097\n DOSPageCount 5\n DOSSiteCount 100\n DOSPageInterval 1\n DOSSiteInterval 1\n DOSBlockingPeriod 10\n",
node:"const rateLimit = require('express-rate-limit');\napp.use('/api/', rateLimit({ windowMs: 60000, max: 60, standardHeaders: true }));\napp.use('/login', rateLimit({ windowMs: 900000, max: 10 }));",
},
"spf":{
title:"SPF DNS Record",
iis:"# Add TXT record to DNS:\n# Name: @ (root domain)\n# Value: v=spf1 include:spf.protection.outlook.com -all\n\n# For Office 365:\nv=spf1 include:spf.protection.outlook.com -all\n# For Google Workspace:\nv=spf1 include:_spf.google.com -all\n# For custom mail server:\nv=spf1 ip4:YOUR.SERVER.IP -all",
nginx:"# SPF is DNS-level — configure at your DNS provider\n# Record type: TXT\n# Name: @ or yourdomain.com\n# Value: v=spf1 ip4:YOUR.IP include:mailprovider.com -all",
apache:"# SPF is DNS-level — configure at your DNS provider\n# Record type: TXT\n# Name: @ or yourdomain.com\n# Value: v=spf1 ip4:YOUR.IP include:mailprovider.com -all",
node:"// SPF is DNS-level — configure at your DNS provider\n// Record: TXT @ \"v=spf1 ip4:YOUR.IP include:mailprovider.com -all\"",
},
"dmarc":{
title:"DMARC DNS Record",
iis:"# Add TXT record to DNS:\n# Name: _dmarc.yourdomain.com\n# Value:\nv=DMARC1; p=quarantine; pct=100; rua=mailto:dmarc@yourdomain.com; adkim=s; aspf=s\n\n# Stages:\n# 1. Start: p=none (monitor only)\n# 2. Move to: p=quarantine\n# 3. Finally: p=reject (full enforcement)",
nginx:"# DMARC is DNS-level:\n# TXT _dmarc.yourdomain.com\n# \"v=DMARC1; p=quarantine; pct=100; rua=mailto:dmarc@yourdomain.com\"",
apache:"# DMARC is DNS-level:\n# TXT _dmarc.yourdomain.com\n# \"v=DMARC1; p=quarantine; pct=100; rua=mailto:dmarc@yourdomain.com\"",
node:"// DMARC is DNS-level:\n// TXT _dmarc.yourdomain.com\n// \"v=DMARC1; p=quarantine; pct=100; rua=mailto:dmarc@yourdomain.com\"",
},
"dmarc_enforcement":{
title:"DMARC Enforcement",
iis:"# Change DMARC TXT record at DNS:\n# From: v=DMARC1; p=none; ...\n# To: v=DMARC1; p=quarantine; pct=100; rua=mailto:dmarc@yourdomain.com\n# Or enforce fully: p=reject",
nginx:"# DMARC enforcement is DNS-level\n# Update TXT _dmarc.yourdomain.com to p=quarantine or p=reject",
apache:"# DMARC enforcement is DNS-level\n# Update TXT _dmarc.yourdomain.com to p=quarantine or p=reject",
node:"// DMARC enforcement is DNS-level\n// Update TXT _dmarc.yourdomain.com to p=quarantine or p=reject",
},
"caa":{
title:"CAA DNS Records",
iis:"# Add CAA records to DNS:\n# Name: yourdomain.com\n# Type: CAA\n# Values:\n0 issue \"letsencrypt.org\"\n0 issue \"digicert.com\"\n0 issuewild \";\"\n0 iodef \"mailto:security@yourdomain.com\"",
nginx:"# CAA is DNS-level — add at your DNS provider:\n# CAA 0 issue \"letsencrypt.org\"\n# CAA 0 issuewild \";\"",
apache:"# CAA is DNS-level — add at your DNS provider:\n# CAA 0 issue \"letsencrypt.org\"\n# CAA 0 issuewild \";\"",
node:"// CAA is DNS-level — add at your DNS provider:\n// CAA 0 issue \"letsencrypt.org\"\n// CAA 0 issuewild \";\"",
},
"mta_sts":{
title:"MTA-STS Setup",
iis:"# 1. Create policy file at https://mta-sts.yourdomain.com/.well-known/mta-sts.txt\nversion: STSv1\nmode: enforce\nmx: mail.yourdomain.com\nmax_age: 604800\n\n# 2. Add DNS TXT record:\n# Name: _mta-sts.yourdomain.com\n# Value: v=STSv1; id=20260101001\n\n# 3. Add TLS-RPT record:\n# Name: _smtp._tls.yourdomain.com\n# Value: v=TLSRPTv1; rua=mailto:tls-reports@yourdomain.com",
nginx:"# Host policy file:\nserver {\n server_name mta-sts.yourdomain.com;\n location /.well-known/mta-sts.txt {\n return 200 \"version: STSv1\\nmode: enforce\\nmx: mail.yourdomain.com\\nmax_age: 604800\";\n add_header Content-Type text/plain;\n }\n}",
apache:"# Host policy file:\nAlias /.well-known/mta-sts.txt /var/www/mta-sts.txt\n# Create file with:\n# version: STSv1\n# mode: enforce\n# mx: mail.yourdomain.com\n# max_age: 604800",
node:"app.get('/.well-known/mta-sts.txt', (req, res) => {\n res.type('text/plain');\n res.send('version: STSv1\\nmode: enforce\\nmx: mail.yourdomain.com\\nmax_age: 604800');\n});",
},
"waf_detected":{
title:"Add WAF Protection",
iis:"# Options for Windows/IIS:\n# 1. Azure Front Door WAF (recommended)\n# → Portal: Front Door > WAF Policy > Create\n# 2. Cloudflare (free tier available)\n# → cloudflare.com > Add Site > DNS proxy\n# 3. IIS URL Rewrite + request filtering:\n\n \n \n \n",
nginx:"# Cloudflare (recommended — free):\n# Change DNS A record to Cloudflare proxy IPs\n# Enable WAF in Cloudflare Dashboard\n\n# ModSecurity (self-hosted):\napt install libapache2-mod-security2 # or nginx-module-security\n# Download OWASP CRS:\ngit clone https://github.com/coreruleset/coreruleset /etc/nginx/modsec/crs",
apache:"# ModSecurity:\napt install libapache2-mod-security2\na2enmod security2\n# Download OWASP CRS:\ngit clone https://github.com/coreruleset/coreruleset /etc/modsecurity/crs\ncp /etc/modsecurity/crs/crs-setup.conf.example /etc/modsecurity/crs/crs-setup.conf",
node:"// Use Cloudflare as reverse proxy (recommended)\n// Or use express-rate-limit + helmet as baseline:\nconst helmet = require('helmet');\nconst rateLimit = require('express-rate-limit');\napp.use(helmet());\napp.use(rateLimit({ windowMs: 60000, max: 100 }));",
},
"https_active":{
title:"Enable HTTPS",
iis:"\n\n \n \n \n \n \n \n \n \n \n",
nginx:"server {\n listen 80;\n server_name yourdomain.com;\n return 301 https://$host$request_uri;\n}\n# Get free cert with Let's Encrypt:\ncertbot --nginx -d yourdomain.com -d www.yourdomain.com",
apache:"\n ServerName yourdomain.com\n Redirect permanent / https://yourdomain.com/\n\n# Get free cert:\ncertbot --apache -d yourdomain.com",
node:"const https = require('https');\nconst fs = require('fs');\nconst opts = { key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem') };\nhttps.createServer(opts, app).listen(443);\n// Redirect HTTP:\nhttp.createServer((req, res) => res.writeHead(301, { Location: 'https://' + req.headers.host + req.url }).end()).listen(80);",
},
"env_exposure":{
title:"Protect .env file",
iis:"\n\n \n \n \n \n \n \n \n \n",
nginx:"location ~ /\\.(env|git|htaccess) {\n deny all;\n return 404;\n}",
apache:"\n Require all denied\n",
node:"// Never serve .env — use environment variables:\n// process.env.MY_SECRET (not from file)\n// Add to .gitignore:\n// .env\n// .env.*",
},
};
// ── Detect server platform from scan ──
function detectPlatform(){
var srv=(proxyData&&proxyData.headers&&proxyData.headers["server"]||"").toLowerCase();
var powered=(proxyData&&proxyData.headers&&proxyData.headers["x-powered-by"]||"").toLowerCase();
if(srv.includes("iis")||powered.includes("asp"))return"iis";
if(srv.includes("nginx"))return"nginx";
if(srv.includes("apache"))return"apache";
if(powered.includes("express")||powered.includes("node"))return"node";
return"iis"; // default for Ituran
}
// ── Show fix modal ──
var fixModal=null;
function showFix(itemId,itemName){
var fix=null;
// find fix by matching item id prefix
Object.keys(FIX_DB).forEach(function(k){
if(itemId&&itemId.toLowerCase().includes(k.toLowerCase())) fix=FIX_DB[k];
});
// try name match
if(!fix){
Object.keys(FIX_DB).forEach(function(k){
if(itemName&&itemName.toLowerCase().includes(k.toLowerCase())) fix=FIX_DB[k];
});
}
if(!fix){
toast("No fix template available for this finding","error",2000);
return;
}
var detectedPlatform=detectPlatform();
var platforms=["iis","nginx","apache","node"];
var platformLabels={"iis":"IIS / web.config","nginx":"Nginx","apache":"Apache","node":"Node.js / Express"};
// Remove old modal if exists
var old=document.getElementById("fixModal");
if(old)old.remove();
var modal=document.createElement("div");
modal.id="fixModal";
modal.className="modal-overlay open";
modal.innerHTML=
'
'+
'
🔧 Fix: '+esc(fix.title)+'
'+
'
'+
platforms.map(function(p){
var isActive=p===detectedPlatform;
return'';
}).join("")+
'
'+
'
'+
'
'+esc(fix[detectedPlatform]||fix.iis||"No fix available for this platform")+'
'+
''+
'
'+
'
'+
(detectedPlatform==="iis"?'⚠ Detected platform: IIS / ASP.NET (based on server headers)':'⚠ Platform auto-detected from server headers — switch if incorrect')+
'
'+
'
'+
''+
''+
'
'+
'
';
modal.addEventListener("click",function(e){if(e.target===modal)modal.remove();});
document.body.appendChild(modal);
fixModal={fix:fix,current:detectedPlatform};
}
function switchFixPlatform(p){
if(!fixModal)return;
fixModal.current=p;
var fc=document.getElementById("fixCode");
if(fc)fc.textContent=fixModal.fix[p]||fixModal.fix.iis||"No fix available for this platform";
var platformLabels={"iis":"IIS / web.config","nginx":"Nginx","apache":"Apache","node":"Node.js / Express"};
["iis","nginx","apache","node"].forEach(function(pl){
var btn=document.getElementById("fixBtn_"+pl);
if(!btn)return;
var isActive=pl===p;
btn.style.borderColor=isActive?"var(--accent)":"var(--border2)";
btn.style.background=isActive?"rgba(0,229,160,.1)":"var(--bg3)";
btn.style.color=isActive?"var(--accent)":"var(--text2)";
btn.textContent=platformLabels[pl]+(isActive?" ✓":"");
});
}
function copyFix(){
var code=document.getElementById("fixCode");
if(!code)return;
navigator.clipboard.writeText(code.textContent).then(function(){
toast("Code copied to clipboard");
}).catch(function(){
// fallback
var ta=document.createElement("textarea");
ta.value=code.textContent;
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
toast("Code copied");
});
}
document.getElementById("remBtn").onclick=function(){
showView("remView");
var rv=document.getElementById("remView");
var items=buildRemItems();
var res=0;items.forEach(function(it,i){if(doneSet.has(it.id+"_"+i))res++;});
var pct=items.length?Math.round((res/items.length)*100):0;
var html='
Remediation Plan
'+
''+res+"/"+items.length+' resolved
'+
'
';
if(!items.length)html+='
No issues to remediate!
';
else{
items.forEach(function(item,i){
var k=item.id+"_"+i,isDone=doneSet.has(k),bc=SC[item.sev]||"#6b7fa8";
// check if fix exists
var hasFix=false;
Object.keys(FIX_DB).forEach(function(fk){
if(item.id&&item.id.toLowerCase().includes(fk.toLowerCase())) hasFix=true;
if(item.name&&item.name.toLowerCase().includes(fk.toLowerCase())) hasFix=true;
});
html+='
';
});
}
rv.innerHTML=html;
};
// FIX: Use proper function call instead of .onclick()
function toggleDone(k,checked){
if(checked)doneSet.add(k);else doneSet.delete(k);
document.getElementById("remBtn").click();
}
document.getElementById("pdfBtn").onclick=function(){
var sc2=curScore||0,gr2=curGrade||"--";
var gc2=sc2>=80?"#00e5a0":sc2>=60?"#f59e0b":"#ff4060";
var allR=Object.values(allData).flat();
var nF=allR.filter(function(r){return r.status==="fail";}).length;
var nW=allR.filter(function(r){return r.status==="warn";}).length;
var nP2=allR.filter(function(r){return r.status==="pass";}).length;
var nI2=allR.filter(function(r){return r.status==="info";}).length;
var targetUrl=document.getElementById("urlInput").value;
// FIX: use global WAF variables instead of local (which were undefined here)
var wafNameSafe = lastWafName || "N/A";
var hasWAFSafe = lastHasWAF || false;
var remItems=buildRemItems();
var remBySev={critical:[],high:[],medium:[],low:[]};
remItems.forEach(function(item){if(remBySev[item.sev])remBySev[item.sev].push(item);});
var SEV_LABELS={critical:{label:"Critical",color:"#ff4060"},high:{label:"High",color:"#f59e0b"},medium:{label:"Medium",color:"#a855f7"},low:{label:"Low",color:"#38bdf8"}};
var catSections=CATS.map(function(cat){
var items=allData[cat.id]||[];if(!items.length)return"";
var col=catColor(cat.id);
return'
'+
'
'+
''+cat.name+''+
''+catLabel(cat.id)+''+
'
'+
items.map(function(item){
var stc=STC[item.status]||"#3d4f6e",bc=SC[item.sev]||"#6b7fa8";
return'
';
var w=window.open("","_blank","width=1000,height=800");
if(w){w.document.write(html);w.document.close();setTimeout(function(){w.print();},800);toast("PDF dialog opened");}
else{
var a=document.createElement("a");
a.href=URL.createObjectURL(new Blob([html],{type:"text/html"}));
a.download="sec-audit-"+Date.now()+".html";a.click();
toast("Downloaded as HTML");
}
};
document.getElementById("dlBtn").onclick=function(){
var allR=Object.values(allData).flat();
var sc2=curScore||0,gr2=curGrade||"--",gc2=sc2>=80?"#00e5a0":sc2>=60?"#f59e0b":"#ff4060";
var rows=CATS.map(function(cat){
return"
"+cat.name+"
"+
(allData[cat.id]||[]).map(function(item){
var stc=STC[item.status]||"#3d4f6e",bc=SC[item.sev]||"#6b7fa8";
return"
"+
""+item.name+" "+
"["+item.sev+"]"+
"
"+item.detail+"
"+
"
";
}).join("");
}).join("");
var html="SEC//AUDIT Report"+
""+
"
';
}
document.getElementById("keyBtn").onclick=function(e){
e.stopPropagation();
document.getElementById("keyModal").classList.add("open");
document.getElementById("keyInput").value=apiKey;
var kst=document.getElementById("kst");
kst.innerHTML=apiKey?
'
Key saved (ends ...'+apiKey.slice(-6)+')
':
'
No key - required outside claude.ai
';
};
document.getElementById("keyCancelBtn").onclick=function(){safeStyle("keyModal","display","none");document.getElementById("keyModal").classList.remove("open");};
document.getElementById("keySaveBtn").onclick=function(){
var v=(document.getElementById("keyInput")||{}).value||"";
v=v.trim();
if(v){apiKey=v;try{localStorage.setItem("sa3_key",v);}catch(e){}}
document.getElementById("keyModal").classList.remove("open");
var kb=document.getElementById("keyBtn");if(kb)kb.style.color=v?"#00e5a0":"";
toast("API key saved");
};
if(apiKey){var kb=document.getElementById("keyBtn");if(kb)kb.style.color="#00e5a0";}
document.getElementById("wafBtn").onclick=function(e){
e.stopPropagation();
var p=document.getElementById("wafPanel");
var isHidden=p.style.display==="none"||p.style.display==="";
p.style.display=isHidden?"block":"none";
if(isHidden){
var rect=this.getBoundingClientRect();
p.style.top=(rect.bottom+6)+"px";
p.style.right=(window.innerWidth-rect.right)+"px";
p.style.left="auto";
var saved=localStorage.getItem("sa3_proxy")||"";
if(saved)document.getElementById("proxyUrl").value=saved;
}
};
document.getElementById("proxyTestBtn").onclick=async function(){
var url=(document.getElementById("proxyUrl").value||"").trim().replace(/\/$/,"");
var statusEl=document.getElementById("proxyStatus");
if(!url){if(statusEl)statusEl.textContent="Enter proxy URL";return;}
if(statusEl){statusEl.textContent="Testing...";statusEl.style.color="var(--text3)";}
try{
var r=await fetch(url+"/ping",{cache:"no-store",signal:AbortSignal.timeout(5000)});
if(r.ok){
var d=await r.json().catch(function(){return{version:"?"};});
proxyConnected=true;
localStorage.setItem("sa3_proxy",url);
var psEl=document.getElementById("proxyStatus");
if(psEl){psEl.textContent="Connected - proxy v"+(d.version||"?");psEl.style.color="var(--accent)";}
var wb=document.getElementById("wafBtn");if(wb)wb.classList.add("active");
var pb=document.getElementById("proxyBadge");if(pb)pb.style.display="inline";
toast("Proxy connected");
}else throw new Error("HTTP "+r.status);
}catch(e){
proxyConnected=false;
var errMsg=e&&e.message?e.message:(e&&e.name==="TimeoutError"?"Timeout after 5s":"Connection refused");
var psEl2=document.getElementById("proxyStatus");
if(psEl2){psEl2.textContent="Failed: "+errMsg;psEl2.style.color="var(--red)";}
var wb=document.getElementById("wafBtn");if(wb)wb.classList.remove("active");
var pb=document.getElementById("proxyBadge");if(pb)pb.style.display="none";
toast("Proxy failed: "+errMsg,"error");
}
};
(function(){
var saved=localStorage.getItem("sa3_proxy")||"";
if(saved){
var puEl=document.getElementById("proxyUrl");
if(puEl) puEl.value=saved;
fetch(saved+"/ping",{cache:"no-store"}).then(function(r){return r.json();}).then(function(d){
proxyConnected=true;
var psEl=document.getElementById("proxyStatus");
if(psEl){psEl.textContent="Connected - proxy v"+d.version;psEl.style.color="var(--accent)";}
var wb=document.getElementById("wafBtn");if(wb)wb.classList.add("active");
var pb=document.getElementById("proxyBadge");if(pb)pb.style.display="inline";
}).catch(function(){proxyConnected=false;});
}
})();
document.getElementById("wafOverride").onchange=function(){
var v=this.value;
var btn=document.getElementById("wafBtn");
if(v&&v!==""){if(btn)btn.textContent=v;}
else{if(btn)btn.innerHTML="⚙ Settings";}
document.getElementById("wafPanel").style.display="none";
};
document.addEventListener("click",function(e){
var p=document.getElementById("wafPanel");if(p&&!p.contains(e.target)&&e.target.id!=="wafBtn")p.style.display="none";
document.getElementById("keyModal").classList.remove("open");
});
document.getElementById("keyModal").addEventListener("click",function(e){e.stopPropagation();});
document.getElementById("mitreBtn").onclick=function(e){e.stopPropagation();showMitreView();};
document.getElementById("dashBtn").onclick=function(e){
e.stopPropagation();
if(Object.keys(allData).length===0){toast("Run a scan first","error");return;}
showDash();
};
document.getElementById("authBtn").onclick=function(){
document.getElementById("authTargetUrl").value=document.getElementById("urlInput").value;
document.getElementById("authLoginUrl").value="";
document.getElementById("authStatus").textContent="";
document.getElementById("authModal").classList.add("open");
};
document.getElementById("authCancelBtn").onclick=function(){document.getElementById("authModal").classList.remove("open");};
document.getElementById("authRunBtn").onclick=async function(){
var targetUrl=document.getElementById("authTargetUrl").value.trim();
var username=document.getElementById("authUser").value.trim();
var password=document.getElementById("authPass").value;
if(!targetUrl||!username||!password){document.getElementById("authStatus").textContent="Required fields missing.";return;}
var proxyUrlVal=(localStorage.getItem("sa3_proxy")||"").trim().replace(/\/$/,"");
if(!proxyConnected||!proxyUrlVal){document.getElementById("authStatus").textContent="Proxy must be connected for auth scan.";return;}
document.getElementById("authStatus").textContent="Running authenticated scan...";
document.getElementById("authRunBtn").disabled=true;
try{
var resp=await fetch(proxyUrlVal+"/scan-auth",{method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify({url:targetUrl,loginUrl:document.getElementById("authLoginUrl").value.trim()||undefined,username:username,password:password})});
await resp.json();
document.getElementById("authModal").classList.remove("open");
toast("Auth scan complete");
}catch(e){document.getElementById("authStatus").textContent="Error: "+e.message;}
document.getElementById("authRunBtn").disabled=false;
};
// ════════════════════════════════════════════════
// CUSTOM CHECK RULES ENGINE
// ════════════════════════════════════════════════
var customRules=[];
try{customRules=JSON.parse(localStorage.getItem("sa_custom_rules")||"[]");}catch(e){}
var RULE_PRESETS={
owasp:[
{id:"r_csp", name:"CSP Missing", type:"header_missing", p1:"content-security-policy", p2:"", sev:"high", message:"Content-Security-Policy header missing — XSS attacks have no mitigation (OWASP A03)"},
{id:"r_hsts", name:"HSTS Missing", type:"header_missing", p1:"strict-transport-security",p2:"", sev:"medium", message:"HSTS missing — SSL stripping possible (OWASP A02)"},
{id:"r_xcto", name:"X-Content-Type Missing",type:"header_missing", p1:"x-content-type-options", p2:"", sev:"medium", message:"X-Content-Type-Options missing — MIME sniffing possible (OWASP A05)"},
{id:"r_cors_wc", name:"Wildcard CORS", type:"header_equals", p1:"access-control-allow-origin",p2:"*", sev:"high", message:"Wildcard CORS — any origin can read responses (OWASP A05)"},
{id:"r_server", name:"Server Header Exposed", type:"header_contains", p1:"server", p2:".", sev:"low", message:"Server header exposes technology fingerprint (OWASP A05)"},
],
headers:[
{id:"r_xfo", name:"X-Frame-Options", type:"header_missing", p1:"x-frame-options", p2:"", sev:"medium", message:"X-Frame-Options missing — clickjacking possible"},
{id:"r_rp", name:"Referrer-Policy", type:"header_missing", p1:"referrer-policy", p2:"", sev:"low", message:"Referrer-Policy missing — URL leakage to third parties"},
{id:"r_pp", name:"Permissions-Policy", type:"header_missing", p1:"permissions-policy", p2:"", sev:"low", message:"Permissions-Policy missing — no browser feature restrictions"},
{id:"r_coep", name:"COEP Missing", type:"header_missing", p1:"cross-origin-embedder-policy",p2:"", sev:"low", message:"Cross-Origin-Embedder-Policy missing"},
{id:"r_coop", name:"COOP Missing", type:"header_missing", p1:"cross-origin-opener-policy",p2:"", sev:"low", message:"Cross-Origin-Opener-Policy missing"},
{id:"r_cache", name:"Cache-Control", type:"header_missing", p1:"cache-control", p2:"", sev:"low", message:"Cache-Control missing — sensitive data may be cached"},
],
api:[
{id:"r_api_ver", name:"API Version in Header", type:"header_contains", p1:"x-api-version", p2:".", sev:"low", message:"API version exposed in header — aids attacker reconnaissance"},
{id:"r_api_err", name:"Stack Trace in Body", type:"body_contains", p1:"", p2:"at Object.",sev:"high",message:"Stack trace exposed in response body — leaks internals"},
{id:"r_api_sql", name:"SQL Error in Body", type:"body_contains", p1:"", p2:"SQL syntax",sev:"critical",message:"SQL error message in response — possible SQLi"},
{id:"r_api_key", name:"API Key in Body", type:"body_contains", p1:"", p2:"sk_live_",sev:"critical",message:"Stripe live API key exposed in response"},
{id:"r_powered", name:"X-Powered-By Exposed", type:"header_contains", p1:"x-powered-by", p2:".", sev:"low", message:"X-Powered-By exposes technology stack"},
],
pci:[
{id:"r_pci_https",name:"HTTPS Required", type:"status_code", p1:"", p2:"301", sev:"critical",message:"PCI-DSS requires HTTPS — HTTP should redirect to HTTPS"},
{id:"r_pci_hsts", name:"HSTS Required (PCI)", type:"header_missing", p1:"strict-transport-security",p2:"", sev:"high", message:"PCI-DSS 4.0 requires HSTS on all cardholder data pages"},
{id:"r_pci_tls", name:"TLS 1.0 Forbidden", type:"body_not_contains", p1:"", p2:"TLSv1\b",sev:"high", message:"PCI-DSS forbids TLS 1.0 — only TLS 1.2+ allowed"},
{id:"r_pci_csp", name:"CSP Required (PCI 6.4)", type:"header_missing", p1:"content-security-policy", p2:"", sev:"high", message:"PCI-DSS 4.0 req 6.4.3 requires Content-Security-Policy"},
],
email:[
{id:"r_spf", name:"SPF Record", type:"dns_spf_missing", p1:"",p2:"", sev:"high", message:"SPF record missing — email spoofing possible"},
{id:"r_spf_sf", name:"SPF Softfail", type:"dns_spf_softfail", p1:"",p2:"", sev:"medium", message:"SPF uses ~all (softfail) — change to -all for strict enforcement"},
{id:"r_dmarc", name:"DMARC Record", type:"dns_dmarc_missing", p1:"",p2:"", sev:"high", message:"DMARC record missing — email spoofing possible"},
{id:"r_dmarc_rej",name:"DMARC not reject", type:"dns_dmarc_not_reject", p1:"",p2:"reject",sev:"medium",message:"DMARC policy is not p=reject — emails may not be blocked"},
{id:"r_caa", name:"CAA Records", type:"dns_caa_missing", p1:"",p2:"", sev:"low", message:"CAA records missing — any CA can issue certificates"},
{id:"r_mtasts", name:"MTA-STS Missing", type:"dns_mta_sts_missing", p1:"",p2:"", sev:"medium", message:"MTA-STS missing — email transport may not be encrypted"},
],
iso27001:[
{id:"iso_csp", name:"ISO A.14: CSP", type:"header_missing", p1:"content-security-policy",p2:"",sev:"high", message:"ISO 27001 A.14.2 — CSP header required for web application security"},
{id:"iso_hsts", name:"ISO A.10: HSTS", type:"header_missing", p1:"strict-transport-security",p2:"",sev:"high",message:"ISO 27001 A.10.1 — HSTS required for cryptographic controls"},
{id:"iso_xcto", name:"ISO A.14: X-Content-Type",type:"header_missing", p1:"x-content-type-options",p2:"",sev:"medium",message:"ISO 27001 A.14.2 — X-Content-Type-Options required"},
{id:"iso_tls", name:"ISO A.10: TLS Required", type:"tls_old_protocol", p1:"",p2:"", sev:"high", message:"ISO 27001 A.10.1 — TLS 1.0/1.1 forbidden, use TLS 1.2+"},
{id:"iso_spf", name:"ISO A.13: SPF", type:"dns_spf_missing", p1:"",p2:"", sev:"high", message:"ISO 27001 A.13.2 — SPF record required for information transfer"},
{id:"iso_dmarc", name:"ISO A.13: DMARC", type:"dns_dmarc_missing", p1:"",p2:"", sev:"high", message:"ISO 27001 A.13.2 — DMARC record required for email security"},
{id:"iso_cookie_s",name:"ISO A.14: Cookie Secure",type:"cookie_missing_secure", p1:"",p2:"", sev:"medium", message:"ISO 27001 A.14.2 — session cookies must have Secure flag"},
{id:"iso_cookie_h",name:"ISO A.14: Cookie HttpOnly",type:"cookie_missing_httponly",p1:"",p2:"", sev:"medium", message:"ISO 27001 A.14.2 — session cookies must have HttpOnly flag"},
],
gdpr:[
{id:"gdpr_https", name:"GDPR Art.32: HTTPS", type:"header_missing", p1:"strict-transport-security",p2:"",sev:"critical",message:"GDPR Art.32 requires encryption in transit — HSTS missing"},
{id:"gdpr_cookie_ss",name:"GDPR: Cookie SameSite",type:"cookie_missing_samesite",p1:"",p2:"", sev:"medium", message:"GDPR requires CSRF protection — cookies missing SameSite attribute"},
{id:"gdpr_csp", name:"GDPR Art.25: CSP", type:"header_missing", p1:"content-security-policy",p2:"",sev:"high", message:"GDPR Art.25 (Privacy by Design) — CSP helps prevent data leakage via XSS"},
{id:"gdpr_rp", name:"GDPR: Referrer-Policy", type:"header_missing", p1:"referrer-policy",p2:"", sev:"medium", message:"GDPR — Referrer-Policy missing, user data may leak to third parties"},
{id:"gdpr_rp_perm",name:"GDPR: Referrer permissive",type:"header_equals", p1:"referrer-policy",p2:"no-referrer-when-downgrade",sev:"medium",message:"GDPR — Referrer-Policy too permissive, use strict-origin"},
{id:"gdpr_pp", name:"GDPR: Permissions-Policy",type:"header_missing", p1:"permissions-policy",p2:"", sev:"low", message:"GDPR — Permissions-Policy missing, browser APIs not restricted"},
{id:"gdpr_server",name:"GDPR: Server header", type:"header_contains", p1:"server",p2:".", sev:"low", message:"GDPR — Server header exposes technology, aids attacker profiling"},
],
wordpress:[
{id:"wp_ver", name:"WordPress Version", type:"body_contains", p1:"",p2:"?ver=",sev:"medium",message:"WordPress version exposed in asset URLs — update or hide version"},
{id:"wp_login", name:"WP Login Exposed", type:"body_contains", p1:"",p2:"wp-login.php",sev:"medium",message:"WordPress login page reference found — consider using custom login URL"},
{id:"wp_api", name:"WP REST API", type:"body_contains", p1:"",p2:"/wp-json/",sev:"low",message:"WordPress REST API endpoint referenced — ensure sensitive endpoints are protected"},
{id:"wp_xmlrpc", name:"XMLRPC Exposed", type:"body_contains", p1:"",p2:"xmlrpc.php",sev:"high",message:"WordPress XMLRPC referenced — disable to prevent brute force attacks"},
{id:"wp_debug", name:"WP Debug Mode", type:"body_contains", p1:"",p2:"wp_debug",sev:"critical",message:"WordPress debug mode ON — sensitive error info may be exposed"},
{id:"wp_readme", name:"README.html", type:"body_contains", p1:"",p2:"readme.html",sev:"low",message:"WordPress readme.html may expose version info"},
{id:"wp_csp", name:"WP Missing CSP", type:"header_missing", p1:"content-security-policy",p2:"",sev:"high",message:"WordPress site missing CSP — vulnerable to XSS via plugins"},
],
azure:[
{id:"az_hsts", name:"Azure: HSTS", type:"header_missing", p1:"strict-transport-security",p2:"",sev:"high",message:"Azure best practice — HSTS should be configured in Azure Front Door"},
{id:"az_csp", name:"Azure: CSP", type:"header_missing", p1:"content-security-policy",p2:"",sev:"high",message:"Azure Security Center — CSP missing"},
{id:"az_cors", name:"Azure: Wildcard CORS", type:"header_equals", p1:"access-control-allow-origin",p2:"*",sev:"high",message:"Azure API Management — wildcard CORS violates Azure security baseline"},
{id:"az_powered", name:"Azure: X-Powered-By", type:"header_contains", p1:"x-powered-by",p2:".",sev:"low",message:"Azure — X-Powered-By exposes ASP.NET version, remove in web.config"},
{id:"az_aspnet", name:"Azure: X-AspNet-Version",type:"header_contains", p1:"x-aspnet-version",p2:".",sev:"low",message:"Azure — X-AspNet-Version exposed, disable in system.web section"},
{id:"az_tls10", name:"Azure: TLS 1.0", type:"tls_old_protocol", p1:"",p2:"", sev:"high", message:"Azure Security Center — TLS 1.0/1.1 deprecated, enforce TLS 1.2+"},
{id:"az_cookie", name:"Azure: Cookie Security", type:"cookie_missing_secure", p1:"",p2:"", sev:"medium", message:"Azure App Service — session cookies must have Secure flag enabled"},
],
financial:[
{id:"sox_https", name:"SOX: HTTPS Enforcement",type:"header_missing", p1:"strict-transport-security",p2:"",sev:"critical",message:"SOX compliance requires HTTPS — HSTS must be configured"},
{id:"sox_csp", name:"SOX: XSS Prevention", type:"header_missing", p1:"content-security-policy",p2:"",sev:"critical",message:"SOX requires XSS protection — CSP header missing"},
{id:"sox_tls", name:"SOX: TLS Protocol", type:"tls_old_protocol", p1:"",p2:"", sev:"critical",message:"SOX compliance — TLS 1.0/1.1 prohibited for financial data"},
{id:"sox_dmarc", name:"SOX: Email Auth", type:"dns_dmarc_missing", p1:"",p2:"", sev:"high", message:"SOX — email authentication (DMARC) required to prevent phishing"},
{id:"sox_cookie", name:"SOX: Session Security", type:"cookie_missing_httponly", p1:"",p2:"", sev:"high", message:"SOX — session cookies must have HttpOnly to prevent credential theft"},
{id:"sox_score", name:"SOX: Min Security Score",type:"score_below", p1:"",p2:"75", sev:"high", message:"SOX — overall security score below minimum threshold of 75"},
{id:"sox_xcto", name:"SOX: Content Type", type:"header_missing", p1:"x-content-type-options",p2:"",sev:"medium",message:"SOX — X-Content-Type-Options required for financial applications"},
],
israel8779:[
{id:"il_https", name:"8779: HTTPS חובה", type:"header_missing", p1:"strict-transport-security",p2:"",sev:"critical",message:"תקן 8779 סעיף 4.2 — HSTS חובה, כל תקשורת חייבת להיות מוצפנת"},
{id:"il_csp", name:"8779: CSP", type:"header_missing", p1:"content-security-policy",p2:"",sev:"high", message:"תקן 8779 סעיף 4.5 — Content-Security-Policy חובה להגנה מ-XSS"},
{id:"il_tls", name:"8779: TLS 1.2+", type:"tls_old_protocol", p1:"",p2:"", sev:"critical",message:"תקן 8779 — TLS 1.0/1.1 אסורים, חובה TLS 1.2 ומעלה"},
{id:"il_cookie", name:"8779: Cookie Secure", type:"cookie_missing_secure", p1:"",p2:"", sev:"high", message:"תקן 8779 — עוגיות session חייבות להיות עם דגל Secure"},
{id:"il_cookieh", name:"8779: Cookie HttpOnly", type:"cookie_missing_httponly", p1:"",p2:"", sev:"high", message:"תקן 8779 — עוגיות session חייבות להיות עם דגל HttpOnly"},
{id:"il_dmarc", name:"8779: DMARC", type:"dns_dmarc_missing", p1:"",p2:"", sev:"high", message:"תקן 8779 — DMARC חובה למניעת זיוף אימייל"},
{id:"il_spf", name:"8779: SPF", type:"dns_spf_missing", p1:"",p2:"", sev:"high", message:"תקן 8779 — SPF חובה להגנה על אימייל ארגוני"},
{id:"il_xframe", name:"8779: Clickjacking", type:"header_missing", p1:"x-frame-options",p2:"", sev:"medium", message:"תקן 8779 — X-Frame-Options חובה למניעת Clickjacking"},
{id:"il_score", name:"8779: ציון מינימלי", type:"score_below", p1:"",p2:"80", sev:"high", message:"תקן 8779 — ציון האבטחה מתחת ל-80, נדרש שיפור מיידי"},
],
};
function saveRules(){
try{localStorage.setItem("sa_custom_rules",JSON.stringify(customRules));}catch(e){}
}
function evaluateRule(rule, scanData){
var h = scanData.headers || {};
var body = (scanData.body || "").toLowerCase();
var sc = scanData.statusCode;
var p1 = (rule.p1||"").toLowerCase();
var p2 = (rule.p2||"").toLowerCase();
var dns = scanData.dns || {};
var cookies = scanData.cookies || [];
var tls = scanData.tls || {};
var tlsDeep = scanData.tlsDeep || {};
try {
switch(rule.type){
// ── Original ──
case "header_missing": return !h[p1];
case "header_contains": return !!(h[p1] && h[p1].toLowerCase().includes(p2));
case "header_not_contains": return !!(h[p1] && !h[p1].toLowerCase().includes(p2));
case "header_equals": return !!(h[p1] && h[p1].toLowerCase().trim()===p2.trim());
case "body_contains": return body.includes(p2);
case "body_not_contains": return !body.includes(p2);
case "status_code": return String(sc)===p2;
case "port_open": return !!(scanData.portScan&&scanData.portScan[parseInt(p1)]&&scanData.portScan[parseInt(p1)].open);
case "port_closed": return !!(scanData.portScan&&scanData.portScan[parseInt(p1)]&&!scanData.portScan[parseInt(p1)].open);
case "score_below": return (scanData.score||0)parseInt(p2||"3000"));
case "body_size_large": return !!(scanData.bodySize && scanData.bodySize>parseInt(p2||"500000"));
default: return false;
}
} catch(e){ return false; }
}
function runCustomRules(scanData){
if(!customRules.length||!scanData) return [];
return customRules.filter(function(r){return r.enabled!==false;}).map(function(rule){
var triggered=evaluateRule(rule, scanData);
return{rule:rule, triggered:triggered};
}).filter(function(r){return r.triggered;});
}
function updateRuleBuilder(){
var type=document.getElementById("ruleTypeInput").value;
var l1=document.getElementById("ruleParam1Label");
var l2=document.getElementById("ruleParam2Label");
var i1=document.getElementById("ruleParam1");
var i2=document.getElementById("ruleParam2");
var w1=document.getElementById("ruleParam1Wrap");
var w2=document.getElementById("ruleParam2Wrap");
// reset
w1.style.display=""; w2.style.display="";
i1.style.display=""; i2.style.display="";
i1.value=""; i2.value="";
switch(type){
// Original types
case "header_missing": l1.textContent="Header name"; i1.placeholder="x-frame-options"; w2.style.display="none"; break;
case "header_contains": l1.textContent="Header name"; l2.textContent="Contains"; i1.placeholder="server"; i2.placeholder="nginx"; break;
case "header_not_contains": l1.textContent="Header name"; l2.textContent="Must not contain"; i1.placeholder="server"; i2.placeholder="apache"; break;
case "header_equals": l1.textContent="Header name"; l2.textContent="Exact value"; i1.placeholder="x-frame-options"; i2.placeholder="DENY"; break;
case "body_contains": w1.style.display="none"; l2.textContent="Body contains"; i2.placeholder="error"; break;
case "body_not_contains": w1.style.display="none"; l2.textContent="Must not contain"; i2.placeholder="password"; break;
case "status_code": w1.style.display="none"; l2.textContent="Status code"; i2.placeholder="200"; break;
case "port_open": l1.textContent="Port number"; w2.style.display="none"; i1.placeholder="6379"; break;
case "port_closed": l1.textContent="Port number"; w2.style.display="none"; i1.placeholder="443"; break;
case "score_below": w1.style.display="none"; l2.textContent="Score threshold"; i2.placeholder="70"; break;
case "redirect_to": w1.style.display="none"; l2.textContent="URL contains"; i2.placeholder="evil.com"; break;
// DNS
case "dns_spf_missing":
case "dns_dmarc_missing":
case "dns_caa_missing":
case "dns_mta_sts_missing": w1.style.display="none"; w2.style.display="none"; break;
case "dns_spf_softfail": w1.style.display="none"; w2.style.display="none"; break;
case "dns_dmarc_not_reject": w1.style.display="none"; l2.textContent="Required policy"; i2.placeholder="reject"; break;
// Cookies
case "cookie_missing_httponly":
case "cookie_missing_secure":
case "cookie_missing_samesite": w1.style.display="none"; w2.style.display="none"; break;
case "cookie_samesite_not_strict": w1.style.display="none"; l2.textContent="Required value"; i2.placeholder="Strict"; break;
// TLS
case "tls_expiry_days": w1.style.display="none"; l2.textContent="Days threshold"; i2.placeholder="30"; break;
case "tls_self_signed":
case "tls_weak_cipher":
case "tls_old_protocol": w1.style.display="none"; w2.style.display="none"; break;
// Performance
case "response_slow": w1.style.display="none"; l2.textContent="Threshold (ms)"; i2.placeholder="3000"; break;
case "body_size_large": w1.style.display="none"; l2.textContent="Max size (bytes)"; i2.placeholder="500000"; break;
default: break;
}
}
function addRule(){
var name=document.getElementById("ruleNameInput").value.trim();
var type=document.getElementById("ruleTypeInput").value;
var p1=document.getElementById("ruleParam1").value.trim();
var p2=document.getElementById("ruleParam2").value.trim();
var sev=document.getElementById("ruleSevInput").value;
var msg=document.getElementById("ruleMessageInput").value.trim();
if(!name){alert("חובה להזין שם לחוק");return;}
var rule={id:"custom_"+Date.now(),name:name,type:type,p1:p1,p2:p2,sev:sev,message:msg||name,enabled:true,custom:true};
customRules.push(rule);
saveRules();
renderRulesList();
// clear inputs
["ruleNameInput","ruleParam1","ruleParam2","ruleMessageInput"].forEach(function(id){document.getElementById(id).value="";});
toast("Rule added: "+name);
}
function deleteRule(id){
customRules=customRules.filter(function(r){return r.id!==id;});
saveRules();
renderRulesList();
toast("Rule deleted","success",1500);
}
function toggleRule(id,enabled){
var r=customRules.find(function(r){return r.id===id;});
if(r) r.enabled=enabled;
saveRules();
renderRulesList();
}
function testRule(){
var el=document.getElementById("ruleTestResult");
if(!proxyData){el.style.color="var(--yellow)";el.textContent="Run a scan first to test rules.";return;}
var type=document.getElementById("ruleTypeInput").value;
var p1=document.getElementById("ruleParam1").value.trim();
var p2=document.getElementById("ruleParam2").value.trim();
var rule={type:type,p1:p1,p2:p2};
var scanData={headers:proxyData.headers||{},body:proxyData.body||"",statusCode:proxyData.statusCode,portScan:proxyData.portScan,score:curScore,redirects:proxyData.redirects};
var triggered=evaluateRule(rule,scanData);
el.style.color=triggered?"#ff4060":"#00e5a0";
el.textContent=triggered?"✗ Rule TRIGGERED on last scan — would create a finding":"✓ Rule not triggered on last scan";
}
function renderRulesList(){
var el=document.getElementById("rulesList");
var countEl=document.getElementById("rulesCount");
if(countEl) countEl.textContent=customRules.length;
if(!customRules.length){
el.innerHTML='
אין חוקים מותאמים אישית. הוסף חוק למעלה או טען תבנית.
';
return;
}
var SC2={critical:"#ff4060",high:"#f59e0b",medium:"#a855f7",low:"#38bdf8"};
el.innerHTML=customRules.map(function(r){
var col=SC2[r.sev]||"#6b7fa8";
return'
';
}).join("");
}
function loadPreset(name){
var preset=RULE_PRESETS[name]||[];
var added=0;
preset.forEach(function(r){
if(!customRules.find(function(x){return x.id===r.id;})){
customRules.push(Object.assign({},r,{enabled:true,custom:false}));
added++;
}
});
saveRules();
renderRulesList();
toast("Added "+added+" rules from "+name+" preset");
}
function exportRules(){
var json=JSON.stringify(customRules,null,2);
var a=document.createElement("a");
a.href=URL.createObjectURL(new Blob([json],{type:"application/json"}));
a.download="sec-audit-rules-"+Date.now()+".json";
a.click();
toast("Rules exported");
}
function importRules(input){
var file=input.files[0];
if(!file)return;
var reader=new FileReader();
reader.onload=function(e){
try{
var imported=JSON.parse(e.target.result);
if(!Array.isArray(imported)){throw new Error("Invalid format");}
var added=0;
imported.forEach(function(r){
if(r.id&&r.name&&r.type&&!customRules.find(function(x){return x.id===r.id;})){
customRules.push(r);added++;
}
});
saveRules();renderRulesList();
toast("Imported "+added+" rules");
}catch(err){toast("Import failed: "+err.message,"error");}
};
reader.readAsText(file);
input.value="";
}
document.getElementById("rulesBtn").onclick=function(){
document.getElementById("rulesModal").classList.add("open");
renderRulesList();
updateRuleBuilder();
};
document.getElementById("rulesModal").addEventListener("click",function(e){e.stopPropagation();});
var multiResults={};
var multiRunning=false;
document.getElementById("multiBtn").onclick=function(){
document.getElementById("multiModal").classList.add("open");
document.getElementById("multiStatus").textContent="";
document.getElementById("multiRunBtn").disabled=false;
document.getElementById("multiRunBtn").textContent="▶ Run Multi Scan";
};
document.getElementById("multiCancelBtn").onclick=function(){
document.getElementById("multiModal").classList.remove("open");
};
document.getElementById("multiRunBtn").onclick=async function(){
var raw=document.getElementById("multiUrls").value.trim();
if(!raw)return;
var urls=raw.split("\n").map(function(u){return u.trim();}).filter(function(u){return u.length>0;}).map(function(u){return u.startsWith("http")?u:"https://"+u;});
if(urls.length===0)return;
if(urls.length>10){document.getElementById("multiStatus").textContent="Max 10 domains at once.";return;}
var depth=document.getElementById("multiDepth").value;
var proxyUrlVal=(localStorage.getItem("sa3_proxy")||"").trim().replace(/\/$/,"");
if(!proxyConnected||!proxyUrlVal){document.getElementById("multiStatus").textContent="Proxy must be connected for multi-scan.";return;}
document.getElementById("multiRunBtn").disabled=true;
document.getElementById("multiRunBtn").textContent="Scanning...";
multiRunning=true;
multiResults={};
document.getElementById("multiModal").classList.remove("open");
showView("multiView");
renderMultiProgress(urls,[]);
var done=[];
var promises=urls.map(async function(url){
try{
var resp=await fetch(proxyUrlVal+"/scan",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url:url,depth:depth})});
var data=await resp.json();
multiResults[url]=data;
}catch(e){
multiResults[url]={error:e.message,url:url,reachable:false};
}
done.push(url);
renderMultiProgress(urls,done);
});
await Promise.allSettled(promises);
multiRunning=false;
renderMultiDashboard(urls);
};
function renderMultiProgress(urls,done){
var mv=document.getElementById("multiView");
var pct=Math.round((done.length/urls.length)*100);
var html='
🔁 Multi Domain Scan
'+
'
'+
'
'+
'Progress'+done.length+'/'+urls.length+'
'+
'
'+
''+
'
'+
'
';
html+=urls.map(function(url){
var isDone=done.indexOf(url)>=0;
var r=multiResults[url];
var gc=r?(r.score>=80?"#00e5a0":r.score>=60?"#f59e0b":"#ff4060"):"#3d4f6e";
var dom="";try{dom=new URL(url).hostname;}catch(e){dom=url;}
return'
';
}).join("");
mv.innerHTML=html;
}
function renderMultiDashboard(urls){
var mv=document.getElementById("multiView");
var results=urls.map(function(url){return{url:url,data:multiResults[url]||{}};}).filter(function(r){return !r.data.error;});
results.sort(function(a,b){return(b.data.score||0)-(a.data.score||0);});
// Bar chart
var maxScore=100;
var barChart=results.map(function(r){
var sc=r.data.score||0;
var gc=sc>=80?"#00e5a0":sc>=60?"#f59e0b":"#ff4060";
var dom="";try{dom=new URL(r.url).hostname;}catch(e){dom=r.url;}
var pct=Math.round((sc/maxScore)*100);
return'
'+
'
'+
''+esc(dom)+''+
''+sc+''+
'
'+
'
'+
''+
'
'+
'
';
}).join("");
// Comparison table
var COMPARE_CATS=["HTTP Headers","TLS / HTTPS","DNS & Email","WAF Detection","Cookie Security","Open Redirect","CSRF Protection"];
var tableHead='
Category
'+
results.map(function(r){
var dom="";try{dom=new URL(r.url).hostname;}catch(e){dom=r.url;}
return'
'+esc(dom.split(".")[0])+'
';
}).join("")+'
';
var CATS_MAP={"HTTP Headers":"headers","TLS / HTTPS":"tls","DNS & Email":"dns","WAF Detection":"waf","Cookie Security":"cookies_sec","Open Redirect":"open_redirect","CSRF Protection":"csrf"};
var STATUS_ICONS={"CLEAN":"✅","ISSUES":"❌","WARN":"⚠","OK":"✅","N/A":"—"};
var tableRows=COMPARE_CATS.map(function(catName){
var cells=results.map(function(r){
// derive status from allData-equivalent in result
var catId=CATS_MAP[catName];
var waf=r.data.waf;
var icon="—";
var color="var(--text3)";
// Simple heuristic from result data
if(catName==="WAF Detection"){
icon=waf&&waf!=="None detected"?"✅":"❌";
color=waf&&waf!=="None detected"?"#00e5a0":"#ff4060";
}
return'
'+icon+'
';
}).join("");
return'
'+catName+'
'+cells+'
';
}).join("");
// Top issues per domain
var issueCards=results.map(function(r){
var sc=r.data.score||0;
var gc=sc>=80?"#00e5a0":sc>=60?"#f59e0b":"#ff4060";
var dom="";try{dom=new URL(r.url).hostname;}catch(e){dom=r.url;}
var wafStr=r.data.waf&&r.data.waf!=="None detected"?r.data.waf:"No WAF";
var wafCol=r.data.waf&&r.data.waf!=="None detected"?"#00e5a0":"#ff4060";
return'
'+
results.map(function(r){
var dom="";try{dom=new URL(r.url).hostname;}catch(e){dom=r.url;}
return'';
}).join("")+
'
';
}
// ── Filter state ──
var activeFilter="all";
function setFilter(f){
activeFilter=f;
// Update button styles
["all","critical","high","medium","low","fail","warn","pass"].forEach(function(id){
var btn=document.getElementById("flt_"+id);
if(!btn)return;
var isActive=id===f;
btn.style.borderColor=isActive?"var(--accent)":"var(--border2)";
btn.style.background=isActive?"rgba(0,229,160,.1)":"var(--bg3)";
btn.style.color=isActive?"var(--accent)":"var(--text2)";
});
// Apply filter to all visible category sections
document.querySelectorAll(".csec").forEach(function(sec){
var cards=sec.querySelectorAll(".card");
var anyVisible=false;
cards.forEach(function(card){
var show=shouldShowCard(card,f);
card.style.display=show?"flex":"none";
if(show)anyVisible=true;
});
// Show/hide "no results" message
var noRes=sec.querySelector(".filter-empty");
if(anyVisible){
if(noRes)noRes.style.display="none";
} else {
if(!noRes){
noRes=document.createElement("div");
noRes.className="filter-empty";
noRes.style.cssText="padding:16px;text-align:center;color:var(--text3);font-family:var(--mono);font-size:10px";
noRes.textContent="No "+f+" findings in this category.";
sec.appendChild(noRes);
}
noRes.style.display="block";
noRes.textContent="No "+f+" findings in this category.";
}
});
}
function shouldShowCard(card,f){
if(f==="all") return true;
var status=card.dataset.status||"info";
var sev=card.dataset.sev||"info";
switch(f){
case "critical": return sev==="critical";
case "high": return sev==="high";
case "medium": return sev==="medium";
case "low": return sev==="low";
case "fail": return status==="fail";
case "warn": return status==="warn";
case "pass": return status==="pass";
default: return true;
}
}
var isLight=false;
try{ isLight=localStorage.getItem("sa_theme")==="light"; }catch(e){}
function applyTheme(light){
isLight=light;
document.documentElement.classList.toggle("light",light);
var btn=document.getElementById("themeBtn");
if(btn) btn.textContent=light?"🌙":"☀️";
try{ localStorage.setItem("sa_theme",light?"light":"dark"); }catch(e){}
}
// Init on load
applyTheme(isLight);
document.getElementById("themeBtn").onclick=function(){
applyTheme(!isLight);
};