PageSourceSearch

https://sitesentry.app/assets/generateSecurityPdf-Cj-R_KIS.js

js sitesentry.app collected 2026-10-09 22:05:18 UTC 67,305 bytes, 2 lines download raw bytes

1import{E as Nt,a as Q}from"./vendor-pdf-DutuzZK6.js";import{S as oe,p as ve,i as _e,c as Ht}from"./generateSecurityReport-bfhFEOdC.js";import{c as It}from"./calculateSecurityScore-Bj22zqZ-.js";import{F as Wt,e as Ot}from"./complianceMapping-BzlJvtF4.js";import{l as $t,f as Ae}from"./reportBranding-CBEA_Gky.js";import"./vendor-charts-Ci_Gx7ja.js";const Mt=[245,245,250],Ut=[60,60,60],Gt=[34,120,215],Yt={ssl_invalid:["Check when your SSL certificate expires by visiting your hosting provider's dashboard or running: openssl s_client -connect yourdomain.com:443 | openssl x509 -noout -dates","Renew your certificate through your provider (Let's Encrypt, Cloudflare, AWS Certificate Manager, etc.). Most providers offer free automatic renewal.","Install the renewed certificate on your web server. For Apache: update SSLCertificateFile and SSLCertificateKeyFile paths. For Nginx: update ssl_certificate and ssl_certificate_key.","Test your SSL configuration at ssllabs.com/ssltest to verify the new certificate is working correctly and has no chain issues."],hsts:["Open your web server configuration file (e.g., nginx.conf, .htaccess for Apache, or your CDN settings).","Add the header: Strict-Transport-Security: max-age=31536000; includeSubDomains - This tells browsers to always use HTTPS for the next year.","If you use a CDN like Cloudflare, enable HSTS in the SSL/TLS settings dashboard instead of server config.","Test by visiting your site and checking the response headers in browser DevTools (Network tab > click any request > Headers)."],csp:["Start with a report-only policy to avoid breaking your site: Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report","Review the violation reports to understand which external resources your site loads (scripts, styles, images, fonts).","Build your final policy by whitelisting only the sources you need: Content-Security-Policy: default-src 'self'; script-src 'self' cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:",'Add the final CSP header to your server config or use a <meta http-equiv="Content-Security-Policy"> tag in your HTML <head>.'],x_frame_options:["Open your web server configuration file or CDN dashboard.","Add the header: X-Frame-Options: DENY (blocks all framing) or X-Frame-Options: SAMEORIGIN (allows your own site to frame itself).",'For Nginx: add_header X-Frame-Options "DENY" always; - For Apache: Header always set X-Frame-Options "DENY"',"Verify by trying to embed your site in an iframe on another page - the browser should block it."],x_content_type_options:["Open your web server configuration or CDN settings.","Add the header: X-Content-Type-Options: nosniff - This is a one-line fix with no downsides.",'For Nginx: add_header X-Content-Type-Options "nosniff" always; - For Apache: Header always set X-Content-Type-Options "nosniff"'],referrer_policy:["Choose a policy: 'strict-origin-when-cross-origin' is the recommended default. It sends the origin (domain) but not the full URL path to external sites.",'Add to your server config - Nginx: add_header Referrer-Policy "strict-origin-when-cross-origin" always;',`Alternative: add <meta name="referrer" content="strict-origin-when-cross-origin"> to your HTML <head> if you can't modify server headers.`],permissions_policy:["Decide which browser features your site actually needs (camera, microphone, geolocation, etc.).","Add the header to deny features you don't use: Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()",'For Nginx: add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;',"If your site uses some features, grant them only to your own origin: Permissions-Policy: camera=(self), geolocation=(self)"],cookie_httponly:["HttpOnly prevents JavaScript from reading your cookies, stopping XSS attacks from stealing session tokens.","In your backend framework, set the HttpOnly flag when creating cookies. For Express.js: res.cookie('session', value, { httpOnly: true })","For PHP: setcookie('session', $value, ['httponly' => true]); - For Django: SESSION_COOKIE_HTTPONLY = True in settings.py","Verify in browser DevTools > Application > Cookies: the HttpOnly column should show a checkmark."],cookie_secure:["The Secure flag ensures cookies are only sent over HTTPS, preventing interception on insecure connections.","In your backend, add the Secure flag: Express.js: res.c
1ookie('session', value, { secure: true })","For PHP: setcookie('session', $value, ['secure' => true]); - For Django: SESSION_COOKIE_SECURE = True","This requires your site to use HTTPS - make sure SSL is properly configured first."],cookie_samesite:["SameSite prevents your cookies from being sent in cross-site requests, blocking CSRF attacks.","Set SameSite=Lax (recommended) or SameSite=Strict: Express.js: res.cookie('session', value, { sameSite: 'lax' })","For PHP: setcookie('session', $value, ['samesite' => 'Lax']); - For Django: SESSION_COOKIE_SAMESITE = 'Lax'","Use 'Strict' for maximum security (cookies won't be sent when following links from other sites) or 'Lax' for a balance."],exposed_paths:["Identify each exposed path and determine if it should be public. Common sensitive paths include /admin, /wp-admin, /.env, /phpinfo.php, /.git.","Block access in your server config - Nginx: location ~ /\\.env { deny all; return 404; } - Apache: add rules to .htaccess.","For admin panels, add authentication: use HTTP basic auth, IP whitelisting, or move to a non-obvious URL path.","Verify the fix: run curl -I https://yoursite.com/.env and confirm you get a 403 or 404 response."],spf:["Go to your DNS provider's dashboard (e.g., Cloudflare, GoDaddy, Namecheap, Route 53).","Add a TXT record for your root domain (@) with the value: v=spf1 include:_spf.google.com ~all (adjust 'include' for your email provider).","If you use multiple email services, combine them: v=spf1 include:_spf.google.com include:sendgrid.net ~all","Verify with: nslookup -type=txt yourdomain.com or use mxtoolbox.com/spf.aspx"],dkim:["Generate DKIM keys through your email provider (Gmail, Microsoft 365, SendGrid, etc.) - each provider has a setup wizard.","Add the DKIM public key as a TXT record in your DNS. The record name is usually: selector._domainkey.yourdomain.com","The value will be a long string starting with 'v=DKIM1; k=rsa; p=...' - copy it exactly from your email provider.","Verify by sending a test email to mail-tester.com or checking with mxtoolbox.com/dkim.aspx"],dmarc:["Start with a monitoring-only policy to collect reports: add a TXT record for _dmarc.yourdomain.com with value: v=DMARC1; p=none; rua=mailto:[email protected]","Review DMARC reports for 2-4 weeks to ensure all legitimate email sources are covered by SPF and DKIM.","Gradually tighten your policy: change p=none to p=quarantine (sends failures to spam), then to p=reject (blocks them entirely).","Final recommended record: v=DMARC1; p=reject; rua=mailto:[email protected]; adkim=s; aspf=s"],dnssec:["Contact your domain registrar (GoDaddy, Namecheap, Cloudflare, etc.) and look for DNSSEC settings in your domain management panel.","Enable DNSSEC - most registrars have a one-click enable button. Cloudflare enables it automatically for domains using their DNS.","If your registrar requires manual DS records, copy the DS record values from your DNS provider and add them at your registrar.","Verify with: dig +short yourdomain.com DS or use dnsviz.net to visualize your DNSSEC chain of trust."],caa:["Decide which Certificate Authorities (CAs) should be allowed to issue certificates for your domain (e.g., Let's Encrypt, DigiCert, Cloudflare).","Add CAA DNS records - for Let's Encrypt only: add a CAA record with flag 0, tag 'issue', value 'letsencrypt.org'",`To allow multiple CAs, add one CAA record per CA. Add an 'iodef' record to get email alerts for unauthorized attempts: 0 iodef "mailto:[email protected]"`,"Verify with: dig CAA yourdomain.com or nslookup -type=caa yourdomain.com"],cors:["Locate your CORS configuration in your server/API code. Look for Access-Control-Allow-Origin headers or CORS middleware.","Replace wildcard (*) or origin-reflecting configurations with a specific allowlist of trusted domains: Access-Control-Allow-Origin: https://yourapp.com","If you need multiple origins, implement server-side logic to check the request origin against an allowlist and respond with the matching origin.","Test by making a cross-origin request from a non-listed domain - it should be blocked by the browser."],mixed_content:["Find all HTTP URLs in your HTML, CSS, and JavaScript files. Search for 'http://' in your codebase.","Change each http:// URL to https:// - or use protocol-relative URLs (//example.com/resource) if the resource supports both.","For images and resources from third parties, verify they support HTTPS. If not, host the resource yourself over HTTPS.",'Add <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"> to your HTML <head> as an extra safety net.'],sri_missing:["For each external script or stylesheet loaded from a CDN, generate an integrity hash. Use: shasum -b -a 384 file.js | xxd -r -p | base64","Or use the online generator at srihash.org - paste the URL of the resource to get the integrity attribute.",'Add the integrity and crossorigin attributes to your script/link tags: <script src="https://cdn.example.com/lib.js" integrity="sha384-abc123..." crossorigin="anonymous">',"When you update a library version, remember to regenerate and update the integrity hash to match the new file."],rate_limiting:["Choose your approach: reverse proxy level (Nginx, Cloudflare) is easiest; application level (Express rate-limit, Django Ratelimit) gives more control.","For Nginx: add 'limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;' in http block, then 'limit_req zone=api burst=20;' in your location block.","For Cloudflare: go to Security > WAF > Rate Limiting Rules and create a rule (e.g., 100 requests per minute per IP).","For Express.js: install 'express-rate-limit' and add: a
1pp.use(rateLimit({ windowMs: 60000, max: 100 }));","Add rate limit headers to help API consumers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset."],open_ports:["Identify why each port is open - is it needed for your application? Common risky ports: 3306 (MySQL), 5432 (PostgreSQL), 27017 (MongoDB), 6379 (Redis).","If the service doesn't need external access, bind it to localhost only. For MySQL: add 'bind-address = 127.0.0.1' in my.cnf.","Add firewall rules to block external access - For UFW: sudo ufw deny 3306; For iptables: iptables -A INPUT -p tcp --dport 3306 -j DROP","If remote access is needed, restrict to specific IPs: sudo ufw allow from 10.0.0.5 to any port 3306","Verify by scanning from an external machine: nmap -p 3306 yourdomain.com - the port should show as filtered or closed."],robots_txt:["Create a robots.txt file in the root of your website (e.g., public/robots.txt or at the web server root).","Add basic rules: User-agent: *\\nDisallow: /admin/\\nDisallow: /api/\\nDisallow: /private/\\nAllow: /","Add a sitemap reference: Sitemap: https://yourdomain.com/sitemap.xml - this helps search engines discover your pages.","Deploy and verify by visiting https://yourdomain.com/robots.txt in your browser. Test with Google Search Console's robots.txt tester."],http2:["HTTP/2 significantly improves page load speed through multiplexing, header compression, and server push.","For Nginx: ensure you have 'listen 443 ssl http2;' in your server block (requires Nginx 1.9.5+).","For Apache: enable mod_http2 with 'a2enmod http2' and add 'Protocols h2 h2c http/1.1' to your VirtualHost.","For Cloudflare or AWS CloudFront: HTTP/2 is enabled by default - no action needed."],http3:["HTTP/3 uses QUIC protocol for even faster connections, especially on mobile and high-latency networks.",`For Nginx: HTTP/3 support requires Nginx 1.25+ compiled with QUIC. Add 'listen 443 quic;' and 'add_header Alt-Svc 'h3=":443"; ma=86400';`,"The easiest path is using Cloudflare (free plan supports HTTP/3) - enable it in Network > HTTP/3 (with QUIC).","For AWS CloudFront: HTTP/3 can be enabled in the distribution settings under 'Supported HTTP versions'."],tls_hardening:["Disable TLS 1.0 and TLS 1.1 in your web server config. Nginx: ssl_protocols TLSv1.2 TLSv1.3; - Apache: SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1","Remove weak cipher suites (RC4, 3DES, EXPORT, NULL, anonymous DH). Nginx: ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305'; ssl_prefer_server_ciphers on;","Enable TLS 1.3 (Nginx 1.13+, Apache 2.4.36+, modern OpenSSL/LibreSSL). It is faster and removes legacy cryptographic primitives.","If you use a CDN (Cloudflare, Fastly, CloudFront), set the minimum TLS version to 1.2 in the dashboard - usually a one-click change.","For Heartbleed (CVE-2014-0160): upgrade OpenSSL to 1.0.1g or later AND reissue every TLS certificate AND rotate every credential that traversed the server before patching.","Re-test with ssllabs.com/ssltest after changes; target an A or A+ grade with no warnings."],subdomain_takeover:["Identify the dangling subdomain and the provider its CNAME targets (e.g. GitHub Pages, Heroku, S3, Azure, Vercel).","Choose one: (a) reclaim the resource on the provider by creating the matching app/bucket/site under your account, or (b) remove the CNAME record from your DNS provider entirely.","If you removed the CNAME, also drop any related ALIAS, ANAME, or wildcard records that may still resolve the subdomain to the provider.","Audit the rest of your DNS for similar dangling records: run 'dig +short CNAME sub.yourdomain.com' for every active subdomain and confirm each target resolves to a resource you control.","Add a DNS hygiene step to your offboarding/decommissioning checklist so future shutdowns also remove DNS records."],secret_leaks:["Treat every exposed credential as compromised. Rotate it immediately via the provider's dashboard (AWS IAM, Stripe Developers, GitHub Settings, etc.).","Audit access logs at the provider for the leaked credential's lifetime to confirm whether it was used by an attacker.","Move the secret out of the client bundle: call the third-party service from an edge function or backend API, and only return the data the browser needs.","Store secrets in your platform's secret manager (Lovable Cloud secrets, AWS Secrets Manager, GCP Secret Manager). Never commit secrets to git or inject them via VITE_* / NEXT_PUBLIC_* env vars (those ship to the browser).","Add a pre-commit secret scanner (gitleaks, trufflehog) to your repo so future leaks are caught before deployment."]};function H(u,G){const $=Yt[G];if(!$||$.length===0)return u.y;const{doc:e,margin:i,contentW:I}=u;let{y:o}=u;o>240&&(e.addPage(),o=18),e.setFillColor(...Mt),e.roundedRect(i,o-2,I,7,1.5,1.5,"F"),e.setFont("helvetica","bold"),e.setFontSize(9),e.setTextColor(...Gt),e.text(">>>  How to Fix - Step by Step",i+3,o+3),o+=10;for(let r=0;r<$.length;r++){o>255&&(e.addPage(),o=18);const le=`${r+1}. ${$[r]}`;e.setFont("helvetica","normal"),e.setFontSize(8.5),e.setTextColor(...Ut);const ae=e.splitTextToSize(le,I-10);e.text(ae,i+5,o),o+=ae.length*3.8+2.5}return o+=3,o}const Et={"Strict-Transport-Security":{label:"[LOCK]",why:"This is like making sure people can only enter your house through the locked front door, not through an open window. Without it, someone could sneak in through an insecure connection and steal your info.",fix:"Tell your web server to add: Strict-Transport-Security: max-age=31536000; includeSubDomains"},"Content-Security-Policy":{label:"[CSP]",why:"Imagine your website is a party. This header is like a bouncer -- it controls who's allowed in. Without it, uninvited guests (malicious scripts) can crash the party and cause cha
1os.",fix:"Define a CSP policy, e.g.: Content-Security-Policy: default-src 'self'; script-src 'self'"},"X-Frame-Options":{label:"[FRAME]",why:"This stops bad guys from putting your website inside a fake frame to trick people into clicking things they shouldn't -- like a wolf in sheep's clothing. It's called clickjacking.",fix:"Add: X-Frame-Options: DENY or SAMEORIGIN"},"X-Content-Type-Options":{label:"[TYPE]",why:"Without this, browsers might guess what type of file they're looking at and get tricked into running something dangerous -- like opening a suspicious package without checking the label first.",fix:"Add: X-Content-Type-Options: nosniff"},"Referrer-Policy":{label:"[REF]",why:"When you click a link, your browser tells the next website where you came from. Without this header, it might share too much info -- like telling a stranger your full home address instead of just your city.",fix:"Add: Referrer-Policy: strict-origin-when-cross-origin"},"Permissions-Policy":{label:"[PERM]",why:"This controls which browser features (camera, microphone, location) your page can use. Without it, sneaky scripts embedded on your page could secretly access your webcam or microphone.",fix:"Add: Permissions-Policy: camera=(), microphone=(), geolocation=()"}},k=[56,176,80],se=[0,172,193],z=[220,53,69],M=[245,158,11],d=[26,32,44],p=[100,110,125],we=[245,250,252],Pe=[0,150,170];let Ce=null;async function Xt(){if(Ce)return Ce;try{const G=await(await fetch("/images/sitesentry-logo.png")).blob();return new Promise($=>{const e=new FileReader;e.onloadend=()=>{Ce=e.result,$(Ce)},e.onerror=()=>$(null),e.readAsDataURL(G)})}catch{return null}}function he(u){return u>=70?k:u>=40?M:z}function ke(u){return u>=90?"Excellent":u>=70?"Good":u>=40?"Needs Work":"Critical"}async function Kt(u,G,$){var me,Se,v,R,ye,_,B,ie,ne,xe,ze,Le,je,De,Ee,Ne,$e,Re,He,Ie,We,Oe,Me,Ue,Ge,Ye,Xe,Ke,Ve,Be,qe,Qe,Je,Ze,et,tt,ot,st,it,nt,rt,lt,at,ct,dt,ut,ht,pt,gt,ft,yt,mt,St,xt,Ct,Tt,bt,Ft,vt,_t,wt,Pt,At,kt,zt,Lt,jt,Dt;const e=new Nt({orientation:"portrait",unit:"mm",format:"a4"}),i=e.internal.pageSize.getWidth(),I=e.internal.pageSize.getHeight(),o=18,r=i-o*2,le=new Date().toLocaleDateString("en-US",{year:"numeric",month:"long",day:"numeric"}),[ae,ce]=await Promise.all([Xt(),$t($==null?void 0:$.orgLogoUrl)]);let K=0,pe=0;for(const s of u)if((me=s.security_json)!=null&&me.headers)for(const a of oe)K++,s.security_json.headers[a]&&pe++;const O=u.map(s=>It(s.security_json,s.ssl_json)),V=O.length>0?Math.round(O.reduce((s,a)=>s+a.total,0)/O.length):0,Te=he(V);e.setFillColor(...we),e.rect(0,0,i,I,"F"),e.setFillColor(...se),e.rect(0,0,i,5,"F"),e.setFillColor(...k),e.rect(0,4,i,1.5,"F");let t=38;ae&&(e.addImage(ae,"PNG",(i-60)/2,t-10,60,22),t+=26),t+=10,e.setFont("helvetica","bold"),e.setFontSize(28),e.setTextColor(...d),e.text("Security Audit Report",i/2,t,{align:"center"}),t+=14,e.setFont("helvetica","normal"),e.setFontSize(13),e.setTextColor(...p),e.text(G,i/2,t,{align:"center"}),t+=8,e.setFontSize(11),e.text(le,i/2,t,{align:"center"});const be=Ae($);if(t+=6,e.setFontSize(9),e.setTextColor(140,140,145),e.text(be,i/2,t,{align:"center"}),ce)try{e.addImage(ce.dataUrl,ce.format,i-o-28,12,28,18,void 0,"FAST")}catch{}t+=25;const Z=i/2;e.setDrawColor(...se),e.setLineWidth(1.2),e.circle(Z,t,24,"S"),e.setFillColor(...Te),e.circle(Z,t,22,"F"),e.setTextColor(255,255,255),e.setFont("helvetica","bold"),e.setFontSize(26),e.text(`${V}%`,Z,t+2,{align:"center"}),e.setFontSize(9),e.setFont("helvetica","normal"),e.text(ke(V),Z,t+10,{align:"center"}),t+=35,e.setTextColor(...p),e.setFontSize(11),e.text(`${u.length} site${u.length!==1?"s":""} scanned  |  ${pe}/${K} headers passed`,Z,t,{align:"center"}),t+=15,e.setFontSize(10),e.setTextColor(150,150,155);const de=e.splitTextToSize("This report explains your website's security in plain language -- no tech jargon needed. Think of it as a health checkup for your online presence.",r-20);e.text(de,Z,t,{align:"center"}),e.setFillColor(...Pe),e.rect(0,I-5,i,5,"F");const P=[],A=[];for(const s of u){const a=s.name;if(s.ssl_json&&!s.ssl_json.valid&&!(!s.ssl_json.issuer&&!s.ssl_json.valid_from&&!s.ssl_json.valid_to&&s.ssl_json.days_left==null&&(s.ssl_json.valid||(Se=s.security_json)!=null&&Se.https_enforced))&&A.push({severity:10,title:"Invalid SSL Certificate",detail:`${a}
1: Your SSL certificate is invalid or expired. Visitors see security warnings and data is transmitted unencrypted.`,monitor:a}),(v=s.security_json)!=null&&v.headers){const m=oe.filter(T=>!s.security_json.headers[T]);if(m.length>=4)A.push({severity:8,title:"Most Security Headers Missing",detail:`${a}: ${m.length} of ${oe.length} security headers are missing, leaving the site vulnerable to XSS, clickjacking, and MIME-type attacks.`,monitor:a});else if(m.length>0)for(const T of m)T==="Content-Security-Policy"&&A.push({severity:7,title:"No Content Security Policy",detail:`${a}: Missing CSP header - the site has no protection against cross-site scripting (XSS) attacks.`,monitor:a})}if((ye=(R=s.security_json)==null?void 0:R.cors)!=null&&ye.misconfigured&&A.push({severity:9,title:"CORS Misconfigured",detail:`${a}: CORS allows unauthorized origins, enabling cross-site data theft from authenticated sessions.`,monitor:a}),((ie=(B=(_=s.security_json)==null?void 0:_.open_ports)==null?void 0:B.open)==null?void 0:ie.length)>0){const{visible:m}=ve(s.security_json.open_ports.open,_e(s.security_json));if(m.length>0){const T=m.map(g=>`${g.port} (${g.service})`).join(", ");
1A.push({severity:8,title:"Open Database/Admin Ports",detail:`${a}: Exposed ports ${T} - attackers can attempt direct connections to backend services.`,monitor:a})}}const f=(ne=s.security_json)==null?void 0:ne.email_security;f&&!((xe=f.spf)!=null&&xe.found)&&!((ze=f.dmarc)!=null&&ze.found)&&A.push({severity:6,title:"No Email Authentication",detail:`${a}: Missing SPF and DMARC records - anyone can send emails pretending to be your domain (phishing/spoofing).`,monitor:a}),(Le=s.security_json)!=null&&Le.rate_limiting&&!s.security_json.rate_limiting.has_rate_limit_headers&&A.push({severity:5,title:"No Rate Limiting",detail:`${a}: No rate limiting detected - the site is vulnerable to brute-force and denial-of-service attacks.`,monitor:a}),((Ee=(De=(je=s.security_json)==null?void 0:je.mixed_content)==null?void 0:De.mixed_content)==null?void 0:Ee.length)>0&&A.push({severity:7,title:"Mixed Content (HTTP on HTTPS)",detail:`${a}: ${s.security_json.mixed_content.mixed_content.length} resource(s) loaded over insecure HTTP, undermining HTTPS protection.`,monitor:a}),(Ne=s.security_json)!=null&&Ne.dnssec&&!s.security_json.dnssec.enabled&&A.push({severity:4,title:"DNSSEC Not Enabled",detail:`${a}: DNS responses are unsigned - visitors could be redirected to malicious sites via DNS spoofing.`,monitor:a}),($e=s.security_json)!=null&&$e.robots_txt&&!s.security_json.robots_txt.exists&&A.push({severity:3,title:"No robots.txt",detail:`${a}: Search engines have unrestricted access to crawl all pages, including potentially sensitive ones.`,monitor:a})}A.sort((s,a)=>a.severity-s.severity);const J=A.slice(0,3);if(J.length>0){e.addPage(),e.setFillColor(...we),e.rect(0,0,i,e.internal.pageSize.getHeight(),"F"),e.setFillColor(...se),e.rect(0,0,i,3,"F"),t=24,e.setFont("helvetica","bold"),e.setFontSize(20),e.setTextColor(...d),e.text("Executive Summary",o,t),t+=4,e.setDrawColor(200,200,210),e.setLineWidth(.4),e.line(o,t,o+r,t),t+=10,e.setFont("helvetica","normal"),e.setFontSize(10),e.setTextColor(...p);const s=`Based on the security scan of ${u.length} site${u.length!==1?"s":""}, here are the top ${J.length} most critical issues that should be addressed first, ranked by potential impact:`,a=e.splitTextToSize(s,r-4);e.text(a,o,t),t+=a.length*4.5+8;const f=[z,M,[245,180,50]],m=["CRITICAL","HIGH","MEDIUM"];for(let C=0;C<J.length;C++){const F=J[C],x=f[Math.min(C,2)];t>240&&(e.addPage(),t=18),e.setFillColor(x[0],x[1],x[2]),e.circle(o+6,t+1,5,"F"),e.setFont("helvetica","bold"),e.setFontSize(12),e.setTextColor(255,255,255),e.text(String(C+1),o+6,t+2.5,{align:"center"});const L=o+14,W=m[Math.min(C,2)];e.setFontSize(7);const q=e.getTextWidth(W)+5;e.setFillColor(x[0],x[1],x[2]),e.roundedRect(L,t-3.5,q,5.5,1.5,1.5,"F"),e.setTextColor(255,255,255),e.text(W,L+q/2,t+.2,{align:"center"}),e.setFont("helvetica","bold"),e.setFontSize(12),e.setTextColor(...d),e.text(F.title,L+q+4,t+1),t+=9,e.setFillColor(248,248,250);const X=e.splitTextToSize(F.detail,r-20),j=X.length*4.2+6;e.roundedRect(o+12,t-3,r-12,j,2,2,"F"),e.setFillColor(x[0],x[1],x[2]),e.rect(o+12,t-3,1.5,j,"F"),e.setFont("helvetica","normal"),e.setFontSize(9),e.setTextColor(70,70,70),e.text(X,o+18,t+1),t+=j+8}t>240&&(e.addPage(),t=18),t+=4,e.setFillColor(235,248,250);const T=J.length===0?"No critical issues detected. Continue monitoring to maintain your security posture.":`Addressing these ${J.length} issues will significantly improve your security posture. Start with #1 and work down the list. Each issue includes step-by-step remediation instructions in the detailed sections that follow.`,g=e.splitTextToSize(T,r-20),S=g.length*4.2+10;if(e.roundedRect(o,t-3,r,S,2,2,"F"),e.setFillColor(...Pe),e.rect(o,t-3,1.5,S,"F"),e.setFont("helvetica","bold"),e.setFontSize(10),e.setTextColor(...Pe),e.text(">>>  Recommendation",o+6,t+2),e.setFont("helvetica","normal"),e.setFontSize(9),e.setTextColor(60,60,60),e.text(g,o+6,t+8),t+=S+10,u.some(C=>C.scoreTrend&&C.scoreTrend.length>=2)){t>210&&(e.addPage(),t=18),e.setFont("helvetica","bold"),e.setFontSize(12),e.setTextColor(...d),e.text("Security Score Trend",o,t),t+=3,e.setFont("helvetica","normal"),e.setFontSize(8),e.setTextColor(...p),e.text("Score changes over the last 5 scans per site",o,t+3),t+=10;for(const C of u){const F=C.scoreTrend;if(!F||F.length<2)continue;t>245&&(e.addPage(),t=18),e.setFont("helvetica","bold"),e.setFontSize(9),e.setTextColor(...d),e.text(C.name,o,t);
1const x=F[F.length-1].score,L=F[F.length-2].score,W=x-L,q=W>0?`+${W}`:String(W),X=W>0?k:W<0?z:p,j=e.getTextWidth(C.name);e.setFont("helvetica","bold"),e.setFontSize(8);const te=he(x);e.setTextColor(te[0],te[1],te[2]),e.text(`${x}`,o+j+4,t),W!==0&&(e.setFontSize(7),e.setTextColor(X[0],X[1],X[2]),e.text(`(${q})`,o+j+4+e.getTextWidth(`${x}`)+2,t)),t+=5;const n=o,c=r,l=18,h=F.map(w=>w.score),y=Math.max(0,Math.min(...h)-10),fe=Math.min(100,Math.max(...h)+10)-y||1;e.setFillColor(248,248,252),e.roundedRect(n,t,c,l,1.5,1.5,"F"),e.setDrawColor(230,230,235),e.setLineWidth(.15);for(let w=0;w<=2;w++){const re=t+l*w/2;e.line(n+2,re,n+c-2,re)}const E=h.map((w,re)=>({px:n+4+(c-8)*re/(h.length-1),py:t+l-2-(w-y)/fe*(l-4)})),U=he(x);e.setFillColor(U[0],U[1],U[2]),e.setGState(new e.GState({opacity:.1})),e.moveTo(E[0].px,t+l-2);for(const w of E)e.lineTo(w.px,w.py);e.lineTo(E[E.length-1].px,t+l-2),e.fill(),e.setGState(new e.GState({opacity:1})),e.setDrawColor(U[0],U[1],U[2]),e.setLineWidth(.6);for(let w=1;w<E.length;w++)e.line(E[w-1].px,E[w-1].py,E[w].px,E[w].py);for(const w of E)e.setFillColor(255,255,255),e.circle(w.px,w.py,1.2,"F"),e.setFillColor(U[0],U[1],U[2]),e.circle(w.px,w.py,.8,"F");e.setFont("helvetica","normal"),e.setFontSize(6),e.setTextColor(160,160,165);for(let w=0;w<F.length;w++){const re=F[w].date.slice(5);e.text(re,E[w].px,t+l+3.5,{align:"center"})}t+=l+10}}}for(const s of u){if(e.addPage(),e.setFillColor(...se),e.rect(0,0,i,3,"F"),t=18,e.setFont("helvetica","bold"),e.setFontSize(18),e.setTextColor(...d),e.text(s.name,o,t),P.push({title:s.name,page:e.getNumberOfPages()}),s.verified!==void 0){const g=s.verified?"VERIFIED":"UNVERIFIED",S=s.verified?k:M,b=e.getTextWidth(s.name),C=o+b+4;e.setFontSize(7);const F=e.getTextWidth(g)+4;e.setFillColor(S[0],S[1],S[2]),e.roundedRect(C,t-4.5,F,5.5,1.5,1.5,"F"),e.setTextColor(255,255,255),e.text(g,C+F/2,t-.8,{align:"center"})}t+=7,e.setFont("helvetica","normal"),e.setFontSize(10),e.setTextColor(...p),e.text(s.url,o,t),t+=4;const a=s.status==="ok"?k:s.status==="warning"?M:z;if(e.setFillColor(a[0],a[1],a[2]),e.roundedRect(o,t,24,6,2,2,"F"),e.setTextColor(255,255,255),e.setFontSize(8),e.text(s.status.toUpperCase(),o+12,t+4.3,{al
1ign:"center"}),t+=14,P.push({title:"SSL Certificate",page:e.getNumberOfPages(),indent:!0}),e.setFont("helvetica","bold"),e.setFontSize(13),e.setTextColor(...d),e.text("SSL Certificate",o,t),t+=7,e.setFont("helvetica","normal"),e.setFontSize(10),s.ssl_json){if(!s.ssl_json.issuer&&!s.ssl_json.valid_from&&!s.ssl_json.valid_to&&s.ssl_json.days_left==null&&(s.ssl_json.valid||(Re=s.security_json)!=null&&Re.https_enforced)){e.setTextColor(...k),e.text("PASS - SSL Active (certificate details not available)",o+2,t),t+=6,e.setTextColor(...p),e.setFontSize(9);const b=e.splitTextToSize("HTTPS is working correctly for this site. Detailed certificate information was not available during the scan, but connections are encrypted.",r-4);e.text(b,o+2,t),t+=b.length*4.5+3}else if(s.ssl_json.valid){e.setTextColor(...k),e.text(`PASS - Valid, expires in ${s.ssl_json.days_left??"unknown"} days`,o+2,t),t+=6,e.setTextColor(...p),e.setFontSize(9);const b=e.splitTextToSize("Think of SSL like an envelope for your letters. It keeps your data sealed so nobody can read it while it's being delivered. Your SSL is good!",r-4);e.text(b,o+2,t),t+=b.length*4.5+3}else{e.setTextColor(...z),e.text("FAIL - Invalid or expired",o+2,t),t+=6,e.setTextColor(...p),e.setFontSize(9);const b=e.splitTextToSize("Your SSL certificate is broken -- it's like sending postcards instead of sealed letters. Anyone snooping on the network can read what your visitors send. This needs fixing ASAP!",r-4);e.text(b,o+2,t),t+=b.length*4.5+3,t=H({doc:e,y:t,margin:o,contentW:r},"ssl_invalid")}s.ssl_json.issuer&&(e.setFontSize(9),e.setTextColor(...p),e.text(`Issuer: ${s.ssl_json.issuer}`,o+2,t),t+=6)}else e.setTextColor(...p),e.text("No SSL data available.",o+2,t),t+=8;const f=(He=s.security_json)==null?void 0:He.tls_analysis;if(f){e.setFont("helvetica","bold"),e.setFontSize(11),e.setTextColor(...d),e.text(`TLS Analysis - Grade ${f.grade}`,o,t),t+=5,e.setFont("helvetica","normal"),e.setFontSize(9),e.setTextColor(...p);const g=f.protocols||{},S=`TLS 1.0: ${g.tls10?"enabled (deprecated)":"disabled"}  |  TLS 1.1: ${g.tls11?"enabled (deprecated)":"disabled"}  |  TLS 1.2: ${g.tls12?"yes":"no"}  |  TLS 1.3: ${g.tls13?"yes":"no"}`,b=e.splitTextToSize(S,r-4);if(e.text(b,o+2,t),t+=b.length*4.5,(Ie=f.cipher)!=null&&Ie.negotiated&&(e.text(`Negotiated cipher: ${f.cipher.negotiated}`,o+2,t),t+=4.5),Array.isArray((We=f.cipher)==null?void 0:We.weak_accepted)&&f.cipher.weak_accepted.length>0){const C=e.splitTextToSize(`Weak ciphers accepted: ${f.cipher.weak_accepted.join("; ")}`,r-4);e.text(C,o+2,t),t+=C.length*4.5}if((Oe=f.cert)!=null&&Oe.sig_alg&&(e.text(`Signature: ${f.cert.sig_alg}`,o+2,t),t+=4.5),(Me=f.cert)!=null&&Me.key_bits&&(e.text(`Public key: ${f.cert.key_type||""} ${f.cert.key_bits}-bit`,o+2,t),t+=4.5),Array.isArray(f.vulnerabilities)&&f.vulnerabilities.length>0){for(const F of f.vulnerabilities){const x=`- [${(F.severity||"").toUpperCase()}] ${F.name}`,L=e.splitTextToSize(x,r-4);e.text(L,o+2,t),t+=L.length*4.2}(f.vulnerabilities.some(F=>F.severity==="high")||g.tls10||g.tls11)&&(t+=2,t=H({doc:e,y:t,margin:o,contentW:r},"tls_hardening"))}t+=6}const m=(Ue=s.security_json)==null?void 0:Ue.subdomain_takeover;if(((Ge=m==null?void 0:m.findings)==null?void 0:Ge.length)>
10){e.setFont("helvetica","bold"),e.setFontSize(11),e.setTextColor(...d);const g=m.findings.filter(S=>S.vulnerable);e.text(`Subdomain Takeover - ${g.length>0?`${g.length} AT RISK`:"Clean"}`,o,t),t+=5,e.setFont("helvetica","normal"),e.setFontSize(9),e.setTextColor(...p);for(const S of m.findings){const b=S.vulnerable?`[${(S.confidence||"").toUpperCase()}] VULNERABLE`:"OK",C=`- ${S.hostname} -> ${S.cname_target} (${S.provider}) ${b}`,F=e.splitTextToSize(C,r-4);if(e.text(F,o+2,t),t+=F.length*4.2,S.vulnerable&&S.evidence_snippet){const x=e.splitTextToSize(`  Evidence: "${S.evidence_snippet}"`,r-4);e.text(x,o+2,t),t+=x.length*4.2}}g.length>0&&(t+=2,t=H({doc:e,y:t,margin:o,contentW:r},"subdomain_takeover")),t+=6}const T=(Ye=s.security_json)==null?void 0:Ye.secret_leaks;if(((Xe=T==null?void 0:T.findings)==null?void 0:Xe.length)>0){e.setFont("helvetica","bold"),e.setFontSize(11),e.setTextColor(...d),e.text(`Exposed Secrets in JS Bundles - ${T.findings.length} finding(s)`,o,t),t+=5,e.setFont("helvetica","normal"),e.setFontSize(9),e.setTextColor(...p),e.text(`Scanned ${T.scanned_scripts} external script(s). Evidence values masked.`,o+2,t),t+=5;for(const g of T.findings){const S=`- [${(g.severity||"").toUpperCase()}] ${g.type}: ${g.evidence_preview}`,b=e.splitTextToSize(S,r-4);e.text(b,o+2,t),t+=b.length*4.2;const C=e.splitTextToSize(`  Source: ${g.source_url}${g.line_hint!=null?` (line ~${g.line_hint})`:""}`,r-4);e.text(C,o+2,t),t+=C.length*4.2}t+=2,t=H({doc:e,y:t,margin:o,contentW:r},"secret_leaks"),t+=6}if(t+=4,e.setFont("helvetica","bold"),e.setFontSize(13),e.setTextColor(...d),P.push({title:"Security Headers",page:e.getNumberOfPages(),indent:!0}),e.text("Security Headers",o,t),t+=3,(Ke=s.security_json)!=null&&Ke.headers){const g=s.security_json.headers,S=s.security_json.header_sources||{},b=oe.filter(n=>g[n]).length,C=Math.round(b/oe.length*100),F=he(C);if(e.setFont("helvetica","normal"),e.setFontSize(10),e.setTextColor(...F),e.text(`${b}/${oe.length} passed (${C}%)`,o+2,t+5),t+=10,s.security_json.is_lovable_hosted){e.setFont("helvetica","italic"),e.setFontSize(9),e.setTextColor(...p);const c=e.splitTextToSize("Hosted on Lovable - platform-managed headers count as satisfied; CSP/Referrer-Policy read from <meta> tags.",i-o*2);e.text(c,o,t),t+=c.length*4+2}const x=oe.map(n=>{const c=!!g[n],l=S[n];return["",n,c?l==="meta"?"Present (meta)":l==="managed"?"Managed":"Present":"Missing"]});Q(e,{startY:t,margin:{left:o,right:o},head:[["","Header","Status"]],body:x,theme:"plain",styles:{fontSize:9,cellPadding:2.5,textColor:[...d]},headStyles:{fillColor:[240,240,245],textColor:[...d],fontStyle:"bold",fontSize:8},columnStyles:{0:{cellWidth:10,halign:"center"},1:{cellWidth:57},2:{cellWidth:22}},didParseCell:n=>{if(n.section==="body"&&n.column.index===2){const l=n.cell.raw!=="Missing";n.cell.styles.textColor=l?[...k]:[...z]}},didDrawCell:n=>{if(n.section==="body"&&n.column.index===0){const l=n.row.raw[2]!=="Missing"?k:z;e.setFillColor(l[0],l[1],l[2]);const h=n.cell.x+n.cell.width/2,y=n.cell.y+n.cell.height/2;e.circle(h,y,1.8,"F")}}}),t=e.lastAutoTable.finalY+4;const L=t+2;e.setFillColor(...k),e.circle(o+3,L,1.5,"F"),e.setFont("helvetica","normal"),e.setFontSize(8),e.setTextColor(...p),e.text("Present",o+7,L+1),e.setFillColor(...z),e.circle(o+28,L,1.5,"F"),e.text("Missing",o+32,L+1),t=L+8;const W=oe.filter(n=>!g[n]);if(W.length>0){e.setFont("helvetica","bold"),e.setFontSize(12),e.setTextColor(...d),e.text("What's Missing & Why It Matters",o,t),t+=7;for(const n of W){const c=Et[n];if(!c)continue;t>250&&(e.addPage(),t=18),e.setFillColor(...we),e.roundedRect(o,t-4,r,6,1,1,"F"),e.setFont("helvetica","bold"),e.setFontSize(10),e.setTextColor(...z),e.text(`${c.label}  ${n}`,o+3,t),t+=6,e.setFont("helvetica","italic"),e.setFontSize(9),e.setTextColor(80,80,80);const l=e.splitTextToSize(`Why it matters: ${c.why}`,r-6);e.text(l,o+3,t),t+=l.length*4+2,e.setFont("helvetica","normal"),e.setFontSize(9),e.setTextColor(...p);const h=e.splitTextToSize(`How to fix: ${c.fix}`,r-6);e.text(h,o+3,t),t+=h.length*4+3;const D={"Strict-Transport-Security":"hsts","Content-Security-Policy":"csp","X-Frame-Options":"x_frame_options","X-Content-Type-Options":"x_content_type_options","Referrer-Policy":"referrer_policy","Permissions-Policy":"permissions_policy"}[n];D&&(t=H({doc:e,y:t,margin:o,contentW:r},D))}}const q=s.security_json.cookie_flags;if(q){t>240&&(e.addPage(),t=18),e.setFont("helvetica","bold"),e.setFontSize(12),e.setTextColor(...d),e.text("Cookie Security Flags",o,t),t+=7;const n=Object.entries(q).map(([l,h])=>["",l,h?"Set":"Missing"]);Q(e,{startY:t,margin:{left:o,right:o},head:[["","Flag","Status"]],body:n,theme:"plain",styles:{fontSize:9,cellPadding:2.5,textColor:[...d]},headStyles:{fillColor:[240,240,245],textColor:[...d],fontStyle:"bold",fontSize:8},columnStyles:{0:{cellWidth:10,halign:"center"},1:{cellWidth:40},2:{cellWidth:22}},didParseCell:l=>{if(l.section==="body"&&l.column.index===2){const h=l.cell.raw;l.cell.styles.textColor=h==="Set"?[...k]:[...z]}},didDrawCell:l=>{if(l.section==="body"&&l.column.index===0){const y=l.row.raw[2]==="Set"?k:z;e.setFillColor(y[0],y[1],y[2]),e.circle(l.cell.x+l.cell.width/2,l.cell.y+l.cell.height/2,1.8,"F")}}}),t=e.lastAutoTable.finalY+6;const c=Object.entries(q).filter(([,l])=>!l).map(([l])=>l.toLowerCase());for(const l of c){const h=`cookie_${l}`;t=H({doc:e,y:t,margin:o,contentW:r},h)}}const X=s.security_json.exposed_paths;if(X&&X.length>0){t>240&&(e.addPage(),t=18),e.setFont("helvetica","bold"),e.setFontSize(12),e.setTextColor(...M),e.text("WARNING - Exposed Sensitive Paths",o,t),t+=7,e.setFont("helvetica","normal"),e.setFontSize(9),e.setTextColor(...p);const c=e.splitTextToSize("These are pages that should probably be hidden but are publicly accessible -- like leaving your diary on the kitchen table.",r-4);e.text(c,o+2,t),t+=c.length*4+4;
1for(const l of X)e.setTextColor(...d),e.text(`- ${l.path}  (HTTP ${l.status})`,o+4,t),t+=5;t=H({doc:e,y:t,margin:o,contentW:r},"exposed_paths")}const j=s.security_json.email_security;if(j){t>240&&(e.addPage(),t=18),e.setFont("helvetica","bold"),e.setFontSize(12),e.setTextColor(...d),P.push({title:"Email Security",page:e.getNumberOfPages(),indent:!0}),e.text("Email Security (SPF / DKIM / DMARC)",o,t),t+=5,e.setFont("helvetica","normal"),e.setFontSize(9),e.setTextColor(...p);const c=e.splitTextToSize("These records prove your domain really sends the emails it claims to. Without them, spammers can impersonate your brand.",r-4);e.text(c,o+2,t),t+=c.length*4+4;const l=[["","SPF",(Ve=j.spf)!=null&&Ve.found?"Found":"Missing",((Be=j.spf)==null?void 0:Be.value)||"-"],["","DKIM",(qe=j.dkim)!=null&&qe.found?"Found":"Missing",(Qe=j.dkim)!=null&&Qe.found?`Key present (selector: ${j.dkim.selector||"default"})`:"-"],["","DMARC",(Je=j.dmarc)!=null&&Je.found?"Found":"Missing",(Ze=j.dmarc)!=null&&Ze.policy?`Policy: ${j.dmarc.policy}`:"-"]];Q(e,{startY:t,margin:{left:o,right:o},head:[["","Check","Status","Details"]],body:l,theme:"plain",styles:{fontSize:9,cellPadding:2.5,textColor:[...d]},headStyles:{fillColor:[240,240,245],textColor:[...d],fontStyle:"bold",fontSize:8},columnStyles:{0:{cellWidth:10,halign:"center"},1:{cellWidth:25},2:{cellWidth:22}},didParseCell:h=>{h.section==="body"&&h.column.index===2&&(h.cell.styles.textColor=h.cell.raw==="Found"?[...k]:[...z])},didDrawCell:h=>{if(h.section==="body"&&h.column.index===0){const D=h.row.raw[2]==="Found"?k:z;e.setFillColor(D[0],D[1],D[2]),e.circle(h.cell.x+h.cell.width/2,h.cell.y+h.cell.height/2,1.8,"F")}}}),t=e.lastAutoTable.finalY+6,(et=j.spf)!=null&&et.found||(t=H({doc:e,y:t,margin:o,contentW:r},"spf")),(tt=j.dkim)!=null&&tt.found||(t=H({doc:e,y:t,margin:o,contentW:r},"dkim")),(ot=j.dmarc)!=null&&ot.found||(t=H({doc:e,y:t,margin:o,contentW:r},"dmarc"))}if(s.security_json.dnssec){t>240&&(e.addPage(),t=18),e.setFont("helvetica","bold"),e.setFontSize(12),e.setTextColor(...d),P.push({title:"DNSSEC",page:e.getNumberOfPages(),indent:!0}),e.text("DNSSEC",o,t),t+=6,e.setFont("helvetica","normal"),e.setFontSize(9);const n=s.security_json.dnssec.enabled,c=n?k:z;e.setTextColor(c[0],c[1],c[2]),e.text(n?"PASS - DNSSEC is enabled":"FAIL - DNSSEC is not enabled",o+2,t),t+=5,e.setTextColor(...p);const l=n?"DNSSEC adds a digital signature to DNS lookups, preventing attackers from redirecting your visitors to fake sites.":"Without DNSSEC, attackers could hijack DNS responses and send your visitors to a malicious copy of your site.",h=e.splitTextToSize(l,r-4);e.text(h,o+2,t),t+=h.length*4+3,n?t+=3:t=H({doc:e,y:t,margin:o,contentW:r},"dnssec")}const te=s.security_json.caa_records;if(te!==void 0){if(t>240&&(e.addPage(),t=18),e.setFont("helvetica","bold"),e.setFontSize(12),e.setTextColor(...d),P.push({title:"CAA Records",page:e.getNumberOfPages(),indent:!0}),e.text("CAA Records",o,t),t+=6,e.setFont("helvetica","normal"),e.setFontSize(9),te.length>0){e.setTextColor(...k),e.text("PASS - CAA records configured",o+2,t),t+=5,e.setTextColor(...p);for(const n of te)e.text(`- ${n}`,o+4,t),t+=4}else{e.setTextColor(...z),e.text("FAIL - No CAA records (any CA can issue certificates)",o+2,t),t+=5,e.setTextColor(...p);const c=e.splitTextToSize("CAA records tell browsers which certificate authorities are allowed to issue SSL certificates for your domain. Without them, a rogue CA could issue a certificate and impersonate your site.",r-4);e.text(c,o+2,t),t+=c.length*4+2,t=H({doc:e,y:t,margin:o,contentW:r},"caa")}t+=3}if(s.security_json.cors){if(t>240&&(e.addPage(),t=18),e.setFont("helvetica","bold"),e.setFontSize(12),e.setTextColor(...d),P.push({title:"CORS Configuration",page:e.getNumberOfPages(),indent:!0}),e.text("CORS Configuration",o,t),t+=6,e.setFont("helvetica","normal"),e.setFontSize(9),s.security_json.cors.misconfigured){e.setTextColor(...z);const n=s.security_json.cors.reflects_origin?"reflects arbitrary origins":"allows wildcard (*)";e.text(`FAIL - Misconfigured: ${n}`,o+2,t),t+=5,e.setTextColor(...p);const l=e.splitTextToSize("CORS controls which other websites can make requests to yours. A misconfiguration means any website could steal data from your visitors' sessions.",r-4);e.text(l,o+2,t),t+=l.length*4+2,t=H({doc:e,y:t,margin:o,contentW:r},"cors")}else e.setTextColor(...k),e.text(`PASS - Properly configured${s.security_json.cors.value?` (${s.security_json.cors.value})`:""}`,o+2,t);t+=6}if(((st=s.security_json.subdomains)==null?void 0:st.count)>0){if(t>240&&(e.addPage(),t=18),e.setFont("helvetica","bold"),e.setFontSize(12),e.setTextColor(...d),e.text(`Discovered Subdomains (${s.security_json.subdomains.count})`,o,t),t+=6,e.setFont("helvetica","normal"),e.setFontSize(9),e.setTextColor(...p),((it=s.security_json.subdomains.risky)==null?void 0:it.length)>
10){e.setTextColor(...M),e.text("Risky subdomains detected:",o+2,t),t+=5,e.setTextColor(...p);for(const n of s.security_json.subdomains.risky)e.text(`- ${n}`,o+4,t),t+=4}else e.text("No risky subdomains found.",o+2,t);t+=6}if((nt=s.security_json.tech)!=null&&nt.detected&&Object.keys(s.security_json.tech.detected).length>0){t>240&&(e.addPage(),t=18),e.setFont("helvetica","bold"),e.setFontSize(12),e.setTextColor(...d),e.text("Technology Stack",o,t),t+=5,e.setFont("helvetica","normal"),e.setFontSize(9),e.setTextColor(...p);const c=e.splitTextToSize("Technologies detected on your site. Outdated or exposed version numbers can help attackers find known vulnerabilities.",r-4);e.text(c,o+2,t),t+=c.length*4+4;const l=Object.entries(s.security_json.tech.detected).map(([h,y])=>[h,String(y)]);if(Q(e,{startY:t,margin:{left:o,right:o},head:[["Technology","Details"]],body:l,theme:"plain",styles:{fontSize:9,cellPadding:2.5,textColor:[...d]},headStyles:{fillColor:[240,240,245],textColor:[...d],fontStyle:"bold",fontSize:8}}),t=e.lastAutoTable.finalY+3,((rt=s.security_json.tech.warnings)==null?void 0:rt.length)>
10){e.setFont("helvetica","normal"),e.setFontSize(9),e.setTextColor(...M);for(const h of s.security_json.tech.warnings)e.text(`WARNING: ${h}`,o+2,t),t+=4}t+=4}if(((at=(lt=s.security_json.vulnerable_libs)==null?void 0:lt.findings)==null?void 0:at.length)>0){t>230&&(e.addPage(),t=18),e.setFont("helvetica","bold"),e.setFontSize(12),e.setTextColor(...z),e.text("Vulnerable JavaScript Libraries",o,t),t+=5,e.setFont("helvetica","normal"),e.setFontSize(9),e.setTextColor(...p);const n=`Scanned ${s.security_json.vulnerable_libs.scanned_scripts??0} script tag(s); known-vulnerable versions matched against a curated CVE database (Retire.js-derived). Maps to OWASP A06:2021 - Vulnerable & Outdated Components.`,c=e.splitTextToSize(n,r-4);e.text(c,o+2,t),t+=c.length*4+4;const l={critical:0,high:1,medium:2,low:3,info:4},y=[...s.security_json.vulnerable_libs.findings].sort((D,fe)=>{const E=l[(D.severity||"low").toLowerCase()]??5,U=l[(fe.severity||"low").toLowerCase()]??5;return E!==U?E-U:`${D.library}`.localeCompare(`${fe.library}`)}).map(D=>{const fe=D.detection==="cdn_url"?"CDN URL pattern":"Filename pattern",E=D.source_url||"";let U=E;try{const ue=new URL(E);U=`${ue.pathname}${ue.search||""}`||E}catch{}const w=(()=>{try{return new URL(E).pathname.split("/").filter(Boolean).pop()||E}catch{return E.split("/").filter(Boolean).pop()||E}})(),re=(D.advisories||[]).map(ue=>ue.id?`${ue.id}${ue.summary?` - ${ue.summary}`:""}
1`:"").filter(Boolean).join(`
2`);return[`${D.library}@${D.version}`,(D.severity||"").toUpperCase(),fe,w,U,E,re]});Q(e,{startY:t,margin:{left:o,right:o},head:[["Library@Version","Severity","Detection","Script Filename","Script Path","Full Source URL","Advisories"]],body:y,theme:"striped",styles:{fontSize:7.5,cellPadding:2,textColor:[...d],overflow:"linebreak",valign:"top"},headStyles:{fillColor:[240,240,245],textColor:[...d],fontStyle:"bold",fontSize:7.5},columnStyles:{0:{cellWidth:26,fontStyle:"bold"},1:{cellWidth:16},2:{cellWidth:22},3:{cellWidth:28},4:{cellWidth:32},5:{cellWidth:40},6:{cellWidth:"auto"}}}),t=e.lastAutoTable.finalY+6}if(s.security_json.mixed_content){const n=s.security_json.mixed_content;if(((ct=n.mixed_content)==null?void 0:ct.length)>0){t>240&&(e.addPage(),t=18),e.setFont("helvetica","bold"),e.setFontSize(12),e.setTextColor(...z),e.text("Mixed Content",o,t),t+=5,e.setFont("helvetica","normal"),e.setFontSize(9),e.setTextColor(...p);const l=e.splitTextToSize("Your HTTPS site loads some resources over insecure HTTP. This is like having a locked front door but leaving a window wide open.",r-4);e.text(l,o+2,t),t+=l.length*4+3;for(const h of n.mixed_content){t>270&&(e.addPage(),t=18),e.setTextColor(...d);const y=e.splitTextToSize(`- ${h}`,r-8);e.text(y,o+4,t),t+=y.length*4}t+=4}if(((dt=n.sri_missing)==null?void 0:dt.length)>0){t>240&&(e.addPage(),t=18),e.setFont("helvetica","bold"),e.setFontSize(12),e.setTextColor(...M),e.text("Missing Subresource Integrity (SRI)",o,t),t+=5,e.setFont("helvetica","normal"),e.setFontSize(9),e.setTextColor(...p);const l=e.splitTextToSize("These external scripts load without integrity checks. If the CDN serving them gets hacked, attackers could inject malicious code into your site.",r-4);e.text(l,o+2,t),t+=l.length*4+3;for(const h of n.sri_missing){t>270&&(e.addPage(),t=18),e.setTextColor(...d);const y=e.splitTextToSize(`- ${h}`,r-8);e.text(y,o+4,t),t+=y.length*4}t+=2,t=H({doc:e,y:t,margin:o,contentW:r},"sri_missing")}}if(s.security_json.rate_limiting){if(t>240&&(e.addPage(),t=18),e.setFont("helvetica","bold"),e.setFontSize(12),e.setTextColor(...d),e.text("Rate Limiting",o,t),t+=6,e.setFont("helvetica","normal"),e.setFontSize(9),s.security_json.rate_limiting.has_rate_limit_headers){e.setTextColor(...k),e.text("PASS - Rate limiting headers detected",o+2,t),t+=5;const n=s.security_json.rate_limiting.headers_found;if(n&&Object.keys(n).length>0){e.setTextColor(...p);for(const[c,l]of Object.entries(n))e.text(`${c}: ${l}`,o+4,t),t+=4}}else{e.setTextColor(...z),e.text("FAIL - No rate limiting headers found",o+2,t),t+=5,e.setTextColor(...p);const c=e.splitTextToSize("Without rate limiting, attackers can flood your server with requests, slowing it down or crashing it entirely (a DDoS attack).",r-4);e.text(c,o+2,t),t+=c.length*4+2,t=H({doc:e,y:t,margin:o,contentW:r},"rate_limiting")}s.security_json.rate_limiting.returns_429&&(e.setTextColor(...M),e.text("WARNING: Server returned 429 Too Many Requests during check",o+2,t),t+=4),t+=4}if(s.security_json.open_ports){t>240&&(e.addPage(),t=18),e.setFont("helvetica","bold"),e.setFontSize(12),e.setTextColor(...d),P.push({title:"Open Port Scan",page:e.getNumberOfPages(),indent:!0}),e.text("Open Port Scan",o,t),t+=5,e.setFont("helvetica","normal"),e.setFontSize(9),e.setTextColor(...p),e.text(`Scanned ${s.security_json.open_ports.scanned} common ports.`,o+2,t),t+=5;const n=_e(s.security_json),{visible:c,suppressed:l}=ve(s.security_json.open_ports.open,n);if(n&&l.length>0){e.setFont("helvetica","italic"),e.setTextColor(...p);const h=Ht(l.length),y=e.splitTextToSize(h,r-4);e.text(y,o+2,t),t+=y.length*4+2,e.setFont("helvetica","normal")}if(c.length>0){const h=c.map(y=>[String(y.port),y.service,y.risk]);Q(e,{startY:t,margin:{left:o,right:o},head:[["Port","Service","Risk"]],body:h,theme:"plain",styles:{fontSize:9,cellPadding:2.5,textColor:[...d]},headStyles:{fillColor:[240,240,245],textColor:[...d],fontStyle:"bold",fontSize:8},didParseCell:y=>{y.section==="body"&&y.column.index===2&&(y.cell.styles.textColor=[...M])}}),t=e.lastAutoTable.finalY+4,t=H({doc:e,y:t,margin:o,contentW:r},"open_ports")}else e.setTextColor(...k),e.text("PASS - No origin-exposed database/admin ports detected",o+2,t),t+=6}if(s.security_json.http_protocols){t>240&&(e.addPage(),t=18),e.setFont("helvetica","bold"),e.setFontSize(12),e.setTextColor(...d),P.push({title:"HTTP Protocol Support",page:e.getNumberOfPages(),indent:!0}),e.text("HTTP Protocol Support",o,t),t+=5,e.setFont("helvetica","normal"),e.setFontSize(9),e.setTextColor(...p);const c=e.splitTextToSize("Newer HTTP versions (HTTP/2 and HTTP/3) make your site load faster and are more secure. Think of them as highway upgrades for your web traffic.",r-4);e.text(c,o+2,t),t+=c.length*4+4;const l=[["HTTP/1.1","Supported"],["HTTP/2",s.security_json.http_protocols.http2?"Supported":"Not detected"],["HTTP/3",s.security_json.http_protocols.http3?"Supported":"Not detected"]];Q(e,{startY:t,margin:{left:o,right:o},head:[["Protocol","Status"]],body:l,theme:"plain",styles:{fontSize:9,cellPadding:2.5,textColor:[...d]},headStyles:{fillColor:[240,240,245],textColor:[...d],fontStyle:"bold",fontSize:8},didParseCell:h=>{h.section==="body"&&h.column.index===1&&(h.cell.styles.textColor=h.cell.raw==="Supported"?[...k]:[...M])}}),t=e.lastAutoTable.finalY+3,s.security_json.http_protocols.alt_svc&&(e.setFont("helvetica","normal"),e.setFontSize(8),e.setTextColor(...p),e.text(`Alt
2-Svc: ${s.security_json.http_protocols.alt_svc}`,o+2,t),t+=4),s.security_json.http_protocols.http2||(t=H({doc:e,y:t,margin:o,contentW:r},"http2")),s.security_json.http_protocols.http3||(t=H({doc:e,y:t,margin:o,contentW:r},"http3")),t+=4}if(s.security_json.robots_txt){if(t>240&&(e.addPage(),t=18),e.setFont("helvetica","bold"),e.setFontSize(12),e.setTextColor(...d),P.push({title:"Robots.txt",page:e.getNumberOfPages(),indent:!0}),e.text("Robots.txt",o,t),t+=6,e.setFont("helvetica","normal"),e.setFontSize(9),s.security_json.robots_txt.exists){if(e.setTextColor(...k),e.text(`PASS - robots.txt found (${s.security_json.robots_txt.disallow_count} Disallow rules${s.security_json.robots_txt.has_sitemap?", Sitemap present":""})`,o+2,t),t+=5,((ut=s.security_json.robots_txt.warnings)==null?void 0:ut.length)>0){e.setTextColor(...M);for(const n of s.security_json.robots_txt.warnings)e.text(`WARNING: ${n}`,o+4,t),t+=4}}else{e.setTextColor(...z),e.text("FAIL - No robots.txt found",o+2,t),t+=5,e.setTextColor(...p);const c=e.splitTextToSize("Without a robots.txt, search engines have unrestricted access to crawl your entire site, including pages you might want to keep private.",r-4);e.text(c,o+2,t),t+=c.length*4+2,t=H({doc:e,y:t,margin:o,contentW:r},"robots_txt")}t+=6}if(s.security_json.csp_analysis){t>240&&(e.addPage(),t=18);const n=s.security_json.csp_analysis;e.setFont("helvetica","bold"),e.setFontSize(12),e.setTextColor(...d),P.push({title:"CSP Analysis",page:e.getNumberOfPages(),indent:!0}),e.text("CSP Analysis",o,t),t+=5,e.setFont("helvetica","normal"),e.setFontSize(9),e.setTextColor(...p);const l=e.splitTextToSize("Content Security Policy controls what resources your page can load. Dangerous directives like 'unsafe-inline' and 'unsafe-eval' weaken XSS protections.",r-4);e.text(l,o+2,t),t+=l.length*4+4;const h=[["unsafe-inline",n.has_unsafe_inline?"Present":"Not used"],["unsafe-eval",n.has_unsafe_eval?"Present":"Not used"],["Wildcard sources",n.has_wildcard?"Present":"Not used"]];Q(e,{startY:t,margin:{left:o,right:o},head:[["Directive","Status"]],body:h,theme:"plain",styles:{fontSize:9,cellPadding:2.5,textColor:[...d]},headStyles:{fillColor:[240,240,245],textColor:[...d],fontStyle:"bold",fontSize:8},didParseCell:y=>{if(y.section==="body"&&y.column.index===1){const D=y.cell.raw;y.cell.styles.textColor=D==="Not used"?[...k]:[...M]}}}),t=e.lastAutoTable.finalY+6}if(s.security_json.owasp_headers){t>240&&(e.addPage(),t=18);const n=s.security_json.owasp_headers;e.setFont("helvetica","bold"),e.setFontSize(12),e.setTextColor(...d),P.push({title:"OWASP Headers",page:e.getNumberOfPages(),indent:!0}),e.text("OWASP Recommended Headers",o,t),t+=5,e.setFont("helvetica","normal"),e.setFontSize(9),e.setTextColor(...p);const l=e.splitTextToSize("Additional security headers recommended by OWASP (COEP, COOP, CORP) that provide extra isolation and protection against side-channel attacks.",r-4);e.text(l,o+2,t),t+=l.length*4+4;const h=Object.entries(n).map(([y,D])=>[y,D?"Present":"Missing"]);Q(e,{startY:t,margin:{left:o,right:o},head:[["Header","Status"]],body:h,theme:"plain",styles:{fontSize:9,cellPadding:2.5,textColor:[...d]},headStyles:{fillColor:[240,240,245],textColor:[...d],fontStyle:"bold",fontSize:8},didParseCell:y=>{if(y.section==="body"&&y.column.index===1){const D=y.cell.raw;y.cell.styles.textColor=D==="Present"?[...k]:[...z]}}}),t=e.lastAutoTable.finalY+6}}else t+=5,e.setFont("helvetica","normal"),e.setFontSize(10),e.setTextColor(...p),e.text("No security scan data available for this site.",o+2,t)}const ee=u.find(s=>s.security_json)||u[0];if(ee!=null&&ee.security_json){e.addPage(),P.push({title:"Compliance Framework Summary",page:e.getNumberOfPages()}),e.setFillColor(250,250,252),e.rect(0,0,i,e.internal.pageSize.getHeight(),"F"),e.setFillColor(...se),e.rect(0,0,i,3,"F"),t=22,e.setFont("helvetica","bold"),e.setFontSize(20),e.setTextColor(...d),e.text("Compliance Framework Summary",o,t),t+=4,e.setDrawColor(200,200,210),e.setLineWidth(.4),e.line(o,t,o+r,t),t+=10,e.setFont("helvetica","normal"),e.setFontSize(10),e.setTextColor(...p);const a=e.splitTextToSize("Your security scan data is automatically mapped to the following compliance frameworks. This summary helps you understand your compliance readiness at a glance.",r-4);e.text(a,o,t),t+=a.length*4.5+8;const f=["soc2","iso27001","pci_dss"];for(const m of f){t>220&&(e.addPage(),t=18);const T=Wt[m],{controls:g,passRate:S}=Ot(m,ee.security_json,ee.ssl_json),b=g.filter(n=>n.result.status==="pass").length,C=g.filter(n=>n.result.status==="partial").length,F=g.filter(n=>n.result.status==="fail").length,x=S>=75?k:S>=50?M:z;e.setFont("helvetica","bold"),e.setFontSize(14),e.setTextColor(...d),e.text(T.name,o,t);const L=e.getTextWidth(T.name);e.setFillColor(x[0],x[1],x[2]);const W=`${S}%`;e.setFontSize(9);const q=e.getTextWidth(W)+6;e.roundedRect(o+L+4,t-4.5,q,6,1.5,1.5,"F"),e.setTextColor(255,255,255),e.text(W,o+L+4+q/2,t-.5,{align:"center"}),t+=4,e.setFont("helvetica","normal"),e.setFontSize(8),e.setTextColor(...p),e.text(`${b} Pass  |  ${C} Partial  |  ${F} Fail  |  ${g.length} Controls`,o,t),t+=5;const X=r,j=4;e.setFillColor(230,230,235),e.roundedRect(o,t,X,j,1.5,1.5,"F"),e.setFillColor(x[0],x[1],x[2]),e.roundedRect(o,t,X*(S/100),j,1.5,1.5,"F"),t+=j+4;
2const te=g.map(n=>[n.id,n.name,n.result.status.toUpperCase(),n.result.evidence]);Q(e,{startY:t,margin:{left:o,right:o},head:[["ID","Control","Status","Evidence"]],body:te,theme:"plain",styles:{fontSize:7.5,cellPadding:2,textColor:[...d],overflow:"linebreak"},headStyles:{fillColor:[230,245,248],textColor:[...d],fontStyle:"bold",fontSize:7.5},columnStyles:{0:{cellWidth:14,fontStyle:"bold"},1:{cellWidth:35},2:{cellWidth:16,halign:"center"},3:{cellWidth:r-65}},didParseCell:n=>{if(n.section==="body"&&n.column.index===2){const c=n.cell.raw.toLowerCase(),l=c==="pass"?k:c==="partial"?M:c==="fail"?z:p;n.cell.styles.textColor=[...l],n.cell.styles.fontStyle="bold"}}}),t=e.lastAutoTable.finalY+10}}const N=[];for(const s of u){const a=s.name;if(s.ssl_json&&!s.ssl_json.valid&&!(!s.ssl_json.issuer&&!s.ssl_json.valid_from&&!s.ssl_json.valid_to&&s.ssl_json.days_left==null&&(s.ssl_json.valid||(ht=s.security_json)!=null&&ht.https_enforced))?N.push({priority:1,category:"SSL",site:a,action:"Renew or replace the invalid/expired SSL certificate"}):((pt=s.ssl_json)==null?void 0:pt.days_left)!=null&&s.ssl_json.days_left<=14&&N.push({priority:2,category:"SSL",site:a,action:`SSL certificate expires in ${s.ssl_json.days_left} days - schedule renewal`}),s.security_json&&s.security_json.https_enforced===!1&&N.push({priority:1,category:"HTTPS",site:a,action:"Enable HTTPS enforcement (redirect HTTP > HTTPS)"}),(gt=s.security_json)!=null&&gt.headers){const m=oe.filter(T=>!s.security_json.headers[T]);for(const T of m){const g=Et[T];N.push({priority:T==="Content-Security-Policy"?2:3,category:"Headers",site:a,action:`Add ${T} header${g?`: ${g.fix}`:""}`})}}if((yt=(ft=s.security_json)==null?void 0:ft.cors)!=null&&yt.misconfigured&&N.push({priority:1,category:"CORS",site:a,action:"Fix CORS configuration - restrict allowed origins to trusted domains only"}),((xt=(St=(mt=s.security_json)==null?void 0:mt.open_ports)==null?void 0:St.open)==null?void 0:xt.length)>0){const{visible:m}=ve(s.security_json.open_ports.open,_e(s.security_json));for(const T of m)N.push({priority:2,category:"Ports",site:a,action:`Close or firewall port ${T.port} (${T.service})`})}const f=(Ct=s.security_json)==null?void 0:Ct.email_security;f&&((Tt=f.spf)!=null&&Tt.found||N.push({priority:3,category:"Email",site:a,action:"Add SPF DNS record to prevent email spoofing"}),(bt=f.dkim)!=null&&bt.found||N.push({priority:3,category:"Email",site:a,action:"Configure DKIM signing for outgoing emails"}),(Ft=f.dmarc)!=null&&Ft.found||N.push({priority:3,category:"Email",site:a,action:"Add DMARC DNS record for email authentication policy"})),(vt=s.security_json)!=null&&vt.dnssec&&!s.security_json.dnssec.enabled&&N.push({priority:4,category:"DNS",site:a,action:"Enable DNSSEC at your DNS provider to prevent DNS spoofing"}),((Pt=(wt=(_t=s.security_json)==null?void 0:_t.mixed_content)==null?void 0:wt.mixed_content)==null?void 0:Pt.length)>0&&N.push({priority:2,category:"Content",site:a,action:`Fix ${s.security_json.mixed_content.mixed_content.length} mixed content resource(s) - load all assets over HTTPS`}),((zt=(kt=(At=s.security_json)==null?void 0:At.mixed_content)==null?void 0:kt.sri_missing)==null?void 0:zt.length)>0&&N.push({priority:4,category:"Content",site:a,action:`Add Subresource Integrity (SRI) hashes to ${s.security_json.mixed_content.sri_missing.length} external script(s)`}),(Lt=s.security_json)!=null&&Lt.rate_limiting&&!s.security_json.rate_limiting.has_rate_limit_headers&&N.push({priority:3,category:"Security",site:a,action:"Implement rate limiting to prevent brute-force and DoS attacks"}),(jt=s.security_json)!=null&&jt.robots_txt&&!s.security_json.robots_txt.exists&&N.push({priority:5,category:"SEO",site:a,action:"Add robots.txt to control search engine crawling of sensitive paths"}),(Dt=s.security_json)!=null&&Dt.caa_records&&(!Array.isArray(s.security_json.caa_records)||s.security_json.caa_records.length===0)&&!Array.isArray(s.security_json.caa_records)&&N.push({priority:5,category:"DNS",site:a,action:"Add CAA DNS records to restrict which CAs can issue certificates"})}if(N.sort((s,a)=>s.priority-a.priority),N.length>0){e.addPage(),P.push({title:"Remediation Checklist",page:e.getNumberOfPages()}),e.setFillColor(250,250,252),e.rect(0,0,i,e.internal.pageSize.getHeight(),"F"),e.setFillColor(...se),e.rect(0,0,i,3,"F"),t=18,e.setFont("helvetica","bold"),e.setFontSize(20),e.setTextColor(...d),e.text("Remediation Checklist",o,t),t+=4,e.setDrawColor(200,200,210),e.setLineWidth(.4),e.line(o,t,o+r,t),t+=8,e.setFont("helvetica","normal"),e.setFontSize(9),e.setTextColor(...p),e.text(`${N.length} action items identified across ${u.length} site${u.length!==1?"s":""}. Address items in order of priority.`,o,t),t+=8;const s=f=>f===1?{label:"CRITICAL",color:z}:f===2?{label:"HIGH",color:M}:f===3?{label:"MEDIUM",color:[245,180,50]}:{label:"LOW",color:[140,140,160]};let a=-1;
2for(const f of N){if(t>265&&(e.addPage(),e.setFillColor(250,250,252),e.rect(0,0,i,e.internal.pageSize.getHeight(),"F"),e.setFillColor(...se),e.rect(0,0,i,3,"F"),t=18),f.priority!==a){a=f.priority,t>16&&(t+=3);const x=s(f.priority);e.setFillColor(x.color[0],x.color[1],x.color[2]),e.setFontSize(7);const L=e.getTextWidth(x.label)+5;e.roundedRect(o,t-3.5,L,5.5,1.5,1.5,"F"),e.setTextColor(255,255,255),e.setFont("helvetica","bold"),e.text(x.label,o+L/2,t+.2,{align:"center"}),t+=6}const m=o+2,T=t-2.8,g=3.5;e.setDrawColor(180,180,190),e.setLineWidth(.35),e.setFillColor(255,255,255),e.roundedRect(m,T,g,g,.6,.6,"FD");const S=m+g+3;e.setFontSize(6),e.setFont("helvetica","bold");const b=e.getTextWidth(f.category)+3.5;e.setFillColor(230,232,240),e.roundedRect(S,T+.2,b,3.2,1,1,"F"),e.setTextColor(80,80,100),e.text(f.category,S+b/2,T+2.6,{align:"center"});const C=S+b+2;e.setFont("helvetica","bold"),e.setFontSize(8),e.setTextColor(...d),e.text(f.site,C,t),t+=4,e.setFont("helvetica","normal"),e.setFontSize(8),e.setTextColor(70,70,80);const F=e.splitTextToSize(f.action,r-10);e.text(F,o+8,t),t+=F.length*3.5+3}}if(P.length>0){e.addPage();const s=e.getNumberOfPages();e.setFillColor(250,250,252),e.rect(0,0,i,e.internal.pageSize.getHeight(),"F"),e.setFillColor(...se),e.rect(0,0,i,3,"F");let a=28;e.setFont("helvetica","bold"),e.setFontSize(20),e.setTextColor(...d),e.text("Table of Contents",o,a),a+=4,e.setDrawColor(200,200,210),e.setLineWidth(.4),e.line(o,a,o+r,a),a+=10;for(const m of P){a>270&&(e.addPage(),e.setFillColor(250,250,252),e.rect(0,0,i,e.internal.pageSize.getHeight(),"F"),e.setFillColor(...se),e.rect(0,0,i,3,"F"),a=20);const T=m.indent?o+8:o,g=m.indent?9.5:11,S=m.indent?"normal":"bold";e.setFont("helvetica",S),e.setFontSize(g);const b=m.indent?p:d;e.setTextColor(b[0],b[1],b[2]),e.text(m.title,T,a);const C=m.page+1;e.setFont("helvetica","normal"),e.setFontSize(9),e.setTextColor(150,150,150),e.text(String(C),i-o,a,{align:"right"});const F=e.getTextWidth(m.title),x=e.getTextWidth(String(C)),L=T+F+3,W=i-o-x-3;W>L+10&&(e.setDrawColor(200,200,210),e.setLineDashPattern([.8,2],0),e.setLineWidth(.3),e.line(L,a-.5,W,a-.5),e.setLineDashPattern([],0)),a+=m.indent?6.5:9}const f=e.getNumberOfPages()-s+1;for(let m=0;m<f;m++)e.movePage(s+m,2+m)}const ge=e.getNumberOfPages(),Fe=Ae($);for(let s=1;s<=ge;s++)e.setPage(s),e.setFont("helvetica","normal"),e.setFontSize(8),e.setTextColor(160,160,160),e.text(`Page ${s} of ${ge}`,i/2,e.internal.pageSize.getHeight()-8,{align:"center"}),s>1&&(e.setFontSize(7),e.setTextColor(170,170,175),e.text(Fe,o,e.internal.pageSize.getHeight()-8));return e}function Rt(u,G){try{const $=u.output("blob"),e=URL.createObjectURL($),i=document.createElement("a");i.href=e,i.download=G,i.style.display="none",document.body.appendChild(i),i.click(),setTimeout(()=>{document.body.removeChild(i),URL.revokeObjectURL(e)},200),window.self!==window.top&&window.open(e,"_blank")}catch{u.save(G)}}async function eo(u,G,$,e){const i=await Kt(u,G,$),I=G.toLowerCase().replace(/[^a-z0-9]+/g,"-"),o=new Date().toISOString().split("T")[0];Rt(i,e??`security-audit-${I}-${o}.pdf`)}async function to(u,G,$,e){const i=new Nt({orientation:"landscape",unit:"mm",format:"a4"}),I=i.internal.pageSize.getWidth(),o=i.internal.pageSize.getHeight(),r=18,le=I-r*2,ae=new Date().toLocaleDateString("en-US",{year:"numeric",month:"long",day:"numeric"});i.setFillColor(250,250,252),i.rect(0,0,I,o,"F");const ce=u.length>0?u[u.length-1].score:0,K=he(ce);i.setFillColor(...K),i.rect(0,0,I,3,"F");const pe=await $t(e==null?void 0:e.orgLogoUrl);if(pe)try{i.addImage(pe.dataUrl,pe.format,I-r-26,8,26,14,void 0,"FAST")}catch{}let O=16;i.setFont("helvetica","bold"),i.setFontSize(18),i.setTextColor(...d),i.text("Security Score History",r,O),i.setFont("helvetica","normal"),i.setFontSize(10),i.setTextColor(...p),i.text(`${G}  |  ${$}  |  ${ae}`,r,O+7),i.setFontSize(8),i.setTextColor(150,150,155),i.text(Ae(e),r,O+12),i.setFillColor(...K),i.roundedRect(I-r-30,O-6,30,14,3,3,"F"),i.setTextColor(255,255,255),i.setFont("helvetica","bold"),i.setFontSize(14),i.text(`${ce}/100`,I-r-15,O+2,{align:"center"}),i.setFontSize(7),i.setFont("helvetica","normal"),i.text(ke(ce),I-r-15,O+7,{align:"center"}),O+=18;const V=u.map(v=>v.score),Te=Math.min(...V),t=Math.max(...V),be=Math.round(V.reduce((v,R)=>v+R,0)/V.length),Z=V.length>=2?V[V.length-1]-V[0]:0;i.setFont("helvetica","normal"),i.setFontSize(9),i.setTextColor(...p),i.text(`Data points: ${u.length}  |  Min: ${Te}  |  Max: ${t}  |  Avg: ${be}  |  Trend: ${Z>=0?"+":""}${Z}`,r,O),O+=8;const Y=r+10,de=le-15,P=o-O-35,A=O+P;i.setFillColor(255,255,255),i.roundedRect(r,O-3,le,P+6,2,2,"F"),i.setDrawColor(230,230,235),i.roundedRect(r,O-3,le,P+6,2,2,"S"),i.setFontSize(7),i.setTextColor(...p);for(let v=0;v<=100;v+=20){const R=A-v/100*P;i.setDrawColor(240,240,245),i.setLineDashPattern([1,1],0),i.line(Y,R,Y+de,R),i.text(String(v),Y-3,R+1,{align:"right"})}const J=A-50/100*P;i.setFillColor(239,68,68),i.setGState(new i.GState({opacity:.03})),i.rect(Y,J,de,A-J,"F");const ee=A-75/100*P;i.setFillColor(245,158,11),i.rect(Y,ee,de,J-ee,"F");const N=A-P;if(i.setFillColor(34,197,94),i.rect(Y,N,de,ee-N,"F"),i.setGState(new i.GState({opacity:1})),u.length>0){const v=de/Math.max(u.length-1,1);i.setFillColor(K[0],K[1],K[2]),i.setGState(new i.GState({opacity:.1}));const R=[];for(let _=0;_<u.length;_++){const B=Y+_*v,ie=A-u[_].score/100*P;R.push([B,ie])}if(R.length>0){i.moveTo(R[0][0],A);for(const[_,B]of R)i.lineTo(_,B);i.lineTo(R[R.length-1][0],A),i.fill()}i.setGState(new i.GState({opacity:1})),i.setDrawColor(K[0],K[1],K[2]),i.setLineWidth(.5),i.setLineDashPattern([],0);for(let _=1;_<u.length;_++){const B=Y+(_-1)*v,ie=A-u[_-1].score/100*P,ne=Y+_*v,xe=A-u[_].score/100*P;i.line(B,ie,ne,xe)}for(let _=0;_<u.length;_++){const B=Y+_*v,ie=A-u[_].score/100*P,ne=he(u[_].score);i.setFillColor(ne[0],ne[1],ne[2]),i.circle(B,ie,1.2,"F")}const ye=Math.max(1,Math.floor(u.length/8));i.setFontSize(6),i.setTextColor(...p);for(let _=0;_<u.length;_+=ye){const B=Y+_*v;i.text(u[_].time,B,A+5,{align:"center",maxWidth:v*ye*.9})}if(u.length>1){const _=Y+(u.length-1)*v;i.text(u[u.length-1].time,_,A+5,{align:"center"})}}i.setFontSize(7),i.setTextColor(180,180,180),i.text("Generated by SiteSentry",I/2,o-8,{align:"center"}),i.addPage(),i.setFillColor(250,250,252),i.rect(0,0,I,o,"F"),i.setFillColor(...K),i.rect(0,0,I,3,"F");let ge=16;i.setFont("helvetica","bold"),i.setFontSize(14),i.setTextColor(...d),i.text("Score History Data",r,ge),ge+=8;const Fe=u.map(v=>{const R=ke(v.score);return[v.time,String(v.score),R]});Q(i,{startY:ge,margin:{left:r,right:r},head:[["Time","Score","Rating"]],body:Fe,theme:"plain",styles:{fontSize:8,cellPadding:2,textColor:[...d]},headStyles:{fillColor:[240,240,245],textColor:[...d],fontStyle:"bold",fontSize:8},columnStyles:{0:{cellWidth:60},1:{cellWidth:20,halign:"center"},2:{cellWidth:30}},didParseCell:v=>{if(v.section==="body"&&v.column.index===1){const R=parseInt(v.cell.raw);v.cell.styles.textColor=[...he(R)],v.cell.styles.fontStyle="bold"}}});const me=G.toLowerCase().replace(/[^a-z0-9]+/g,"-"),Se=new Date().toISOString().split("T")[0];Rt(i,`score-history-${me}-${Se}.pdf`)}export{to as downloadScoreHistoryPdf,eo as downloadSecurityPdf,Kt as generateSecurityPdf};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.