88from pathlib import Path
99import shutil
1010from datetime import datetime , timedelta
11+ import socket
12+ import ssl
13+ import urllib .request
14+ import urllib .parse
1115
1216def run_nmap (ip , output_dir = None ):
1317 os .makedirs (output_dir , exist_ok = True )
@@ -160,7 +164,7 @@ def parse_nmap_output(xml_file):
160164
161165 return vulnerabilities , scan_info
162166
163- def generate_html (vulnerabilities , scan_info , target_ip , output_dir = None ):
167+ def generate_html (vulnerabilities , scan_info , target_ip , output_dir = None , crypto_data = None ):
164168 """Generate HTML report for security vulnerabilities."""
165169
166170 severity_counts = {
@@ -595,6 +599,8 @@ def generate_html(vulnerabilities, scan_info, target_ip, output_dir=None):
595599 </div>
596600 ''' if not is_clean else '' }
597601
602+ { get_crypto_html_snippets (crypto_data ) if crypto_data else '' }
603+
598604 <footer>
599605 <p>Generated by Zeuz Security Automation Framework • { current_datetime } </p>
600606 </footer>
@@ -675,7 +681,11 @@ def generate_html(vulnerabilities, scan_info, target_ip, output_dir=None):
675681
676682def nmap_scan_run (url , security_report_dir = None ):
677683 ip_address = url
678- security_report_dir .mkdir (parents = True , exist_ok = True )
684+ if hasattr (security_report_dir , 'mkdir' ):
685+ security_report_dir .mkdir (parents = True , exist_ok = True )
686+ elif security_report_dir :
687+ os .makedirs (security_report_dir , exist_ok = True )
688+
679689 print (f"Saving all reports directly to: { security_report_dir } " )
680690 print ("Running Nmap scan. It may take a while..." )
681691 xml_result , text_result = run_nmap (ip_address , security_report_dir )
@@ -686,11 +696,337 @@ def nmap_scan_run(url, security_report_dir=None):
686696 print ("Parsing results..." )
687697 vuln_data , scan_info = parse_nmap_output (xml_result )
688698
699+ print ("Running Cryptography & Surface Level scan (Background)..." )
700+ crypto_data = get_cryptography_data (ip_address )
701+
689702 print ("Generating HTML report..." )
690- html_result = generate_html (vuln_data , scan_info , ip_address , security_report_dir )
703+ html_result = generate_html (vuln_data , scan_info , ip_address , security_report_dir , crypto_data )
691704
692705 return {
693706 "xml" : xml_result ,
694707 "txt" : text_result ,
708+ "html" : html_result ,
709+ "crypto" : crypto_data
710+ }
711+
712+ def get_crypto_html_snippets (crypto_data ):
713+ ssl_info = crypto_data .get ('ssl' , {})
714+ headers_info = crypto_data .get ('headers' , {})
715+ banner_info = crypto_data .get ('banner' , {})
716+ whois_info = crypto_data .get ('whois' , {})
717+ geo_info = crypto_data .get ('geo' , {})
718+ osint_info = crypto_data .get ('osint' , {})
719+
720+ def badge (value , expected = None , good = 'Good' , is_tls = False ):
721+ if is_tls :
722+ if 'Good' in value : return f'<span class="badge" style="background:var(--success); color:white;">{ value } </span>'
723+ else : return f'<span class="badge" style="background:var(--danger); color:white;">{ value } </span>'
724+
725+ if isinstance (value , str ) and value .lower () in ['missing' , 'invalid' , 'hidden' , 'unknown' , 'true' , 'false' ]:
726+ if value .lower () == 'true' and expected is False :
727+ return f'<span class="badge" style="background:var(--danger); color:white;">Yes</span>'
728+ elif value .lower () == 'false' and expected is False :
729+ return f'<span class="badge" style="background:var(--success); color:white;">No</span>'
730+ elif value .lower () == 'missing' or (value .lower () == 'true' ):
731+ return f'<span class="badge" style="background:var(--warning); color:white;">{ value } </span>'
732+ return f'<span class="badge badge-secondary" style="background:#e2e8f0; color:#475569;">{ value } </span>'
733+
734+ if expected is not None :
735+ if value == expected :
736+ return f'<span class="badge" style="background:var(--success); color:white;">{ good } </span>'
737+ else :
738+ if ssl_info .get ('blocked' ):
739+ return f'<span class="badge" style="background:var(--warning); color:white;">Domain Blocked</span>'
740+ return f'<span class="badge" style="background:var(--danger); color:white;">High Risk</span>'
741+
742+ return f'<span class="badge" style="background:var(--primary); color:white;">{ value } </span>'
743+
744+ tls_versions_html = ""
745+ for tls_ver , strength in ssl_info .get ("tls_versions" , []):
746+ tls_versions_html += f"<div>{ tls_ver } : { badge (strength , is_tls = True )} </div>"
747+ if not tls_versions_html :
748+ tls_versions_html = "<div><span class='badge badge-secondary' style='background:#e2e8f0; color:#475569;'>Unknown - Detection failed</span></div>"
749+
750+ ssl_error_html = f" <span style='color:var(--danger); font-size:13px; font-weight:500; margin-left:8px;'>({ ssl_info .get ('error' , 'Validation Failed' )} )</span>" if not ssl_info .get ('valid' ) and ssl_info .get ('error' ) else ""
751+
752+ osint_html = ""
753+ if osint_info .get ('subdomains' ):
754+ osint_html = f"""
755+ <div class="card">
756+ <div class="section-title">OSINT & Web Footprint</div>
757+ <table>
758+ <tbody>
759+ <tr><td width="30%">Public Subdomains (Top 15 via crt.sh & HackerTarget)</td><td>{ '<br>' .join (f"<span>{ s } </span>" for s in osint_info ['subdomains' ])} </td></tr>
760+ </tbody>
761+ </table>
762+ </div>"""
763+
764+ return f"""
765+ <div class="card">
766+ <div class="section-title">SSL / TLS Configuration</div>
767+ <table>
768+ <tbody>
769+ <tr><td width="30%">Valid Certificate</td><td>{ badge (ssl_info .get ('valid' , 'Unknown' ), expected = True , good = 'Yes' )} { ssl_error_html } </td></tr>
770+ <tr><td>Expired</td><td>{ badge (str (ssl_info .get ('expired' , 'Unknown' )), expected = False , good = 'No' )} </td></tr>
771+ <tr><td>Self-Signed</td><td>{ badge (str (ssl_info .get ('self_signed' , 'Unknown' )), expected = False , good = 'No' )} </td></tr>
772+ <tr><td>Wrong Hostname</td><td>{ badge (str (ssl_info .get ('wrong_hostname' , 'Unknown' )), expected = False , good = 'No' )} </td></tr>
773+ <tr><td>Issuer</td><td><span class="badge badge-secondary" style="background:#e2e8f0; color:#475569;">{ ssl_info .get ('issuer' , 'Unknown' )} </span></td></tr>
774+ <tr><td>Expiry Date</td><td><span class="badge badge-secondary" style="background:#e2e8f0; color:#475569;">{ ssl_info .get ('not_after' , 'Unknown' )} </span></td></tr>
775+ <tr><td>Supported TLS Versions</td><td>{ tls_versions_html } </td></tr>
776+ </tbody>
777+ </table>
778+ </div>
779+
780+ <div class="card">
781+ <div class="section-title">Security Headers</div>
782+ <table>
783+ <tbody>
784+ <tr><td width="30%">Strict-Transport-Security</td><td>{ badge (headers_info .get ('Strict-Transport-Security' , 'Missing' ))} </td></tr>
785+ <tr><td>Content-Security-Policy</td><td>{ badge (headers_info .get ('Content-Security-Policy' , 'Missing' ))} </td></tr>
786+ <tr><td>X-Content-Type-Options</td><td>{ badge (headers_info .get ('X-Content-Type-Options' , 'Missing' ))} </td></tr>
787+ </tbody>
788+ </table>
789+ </div>
790+
791+ <div class="card">
792+ <div class="section-title">Server Banner Information</div>
793+ <table>
794+ <tbody>
795+ <tr><td width="30%">Server</td><td><span class="badge" style="background:var(--primary); color:white;">{ banner_info .get ('Server' , 'Hidden' )} </span></td></tr>
796+ <tr><td>X-Powered-By</td><td><span class="badge" style="background:var(--warning); color:white;">{ banner_info .get ('X-Powered-By' , 'Hidden' )} </span></td></tr>
797+ </tbody>
798+ </table>
799+ </div>
800+
801+ <div class="card">
802+ <div class="section-title">WHOIS Information</div>
803+ <table>
804+ <tbody>
805+ <tr><td width="30%">Registrar</td><td><strong>{ whois_info .get ('Registrar' , 'Unknown' )} </strong></td></tr>
806+ <tr><td>Creation Date</td><td>{ whois_info .get ('Creation Date' , 'Unknown' )} </td></tr>
807+ <tr><td>Expiry Date</td><td>{ whois_info .get ('Expiry Date' , 'Unknown' )} </td></tr>
808+ </tbody>
809+ </table>
810+ </div>
811+
812+ <div class="card">
813+ <div class="section-title">IP Geolocation</div>
814+ <table>
815+ <tbody>
816+ <tr><td width="30%">IP Address</td><td><strong>{ geo_info .get ('IP' , 'Unknown' )} </strong></td></tr>
817+ <tr><td>Country</td><td>{ geo_info .get ('Country' , 'Unknown' )} </td></tr>
818+ <tr><td>Hosting Provider / ISP</td><td>{ geo_info .get ('Hosting Provider' , 'Unknown' )} </td></tr>
819+ </tbody>
820+ </table>
821+ </div>
822+ { osint_html }
823+ """
824+
825+ def generate_crypto_html (report_data , target , output_dir = None ):
826+ current_datetime = datetime .now ().strftime ("%Y-%m-%d %H:%M:%S" )
827+
828+ domain = report_data .get ('domain' , target )
829+
830+ html_template = f"""
831+ <!DOCTYPE html>
832+ <html lang="en">
833+ <head>
834+ <meta charset="UTF-8">
835+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
836+ <title>Cryptography Scan Report - { domain } </title>
837+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
838+ <style>
839+ :root {{
840+ --primary: #2563eb;
841+ --secondary: #64748b;
842+ --success: #10b981;
843+ --warning: #f59e0b;
844+ --danger: #ef4444;
845+ --dark: #0f172a;
846+ --light: #f8fafc;
847+ --surface: #ffffff;
848+ --border: #e2e8f0;
849+ }}
850+ body {{ font-family: 'Inter', sans-serif; margin: 0; padding: 0; background-color: #f1f5f9; color: #334155; line-height: 1.6; }}
851+ .container {{ max-width: 1000px; margin: 0 auto; padding: 20px; }}
852+ .header {{ background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); color: white; padding: 40px 0; margin-bottom: -60px; padding-bottom: 80px; }}
853+ .header-content {{ max-width: 1000px; margin: 0 auto; padding: 0 20px; display: flex; justify-content: space-between; align-items: center; }}
854+ .logo h1 {{ margin: 0; font-size: 24px; font-weight: 700; letter-spacing: -0.5px; }}
855+ .logo p {{ margin: 5px 0 0; opacity: 0.8; font-size: 14px; }}
856+ .card {{ background: white; border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); padding: 24px; margin-bottom: 24px; border: 1px solid var(--border); }}
857+ .section-title {{ font-size: 18px; font-weight: 600; color: var(--dark); margin-top: 0; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid var(--border); }}
858+ table {{ width: 100%; border-collapse: collapse; }}
859+ th {{ text-align: left; padding: 12px 16px; font-size: 12px; font-weight: 600; text-transform: uppercase; color: var(--secondary); background: #f8fafc; border-bottom: 1px solid var(--border); }}
860+ td {{ padding: 12px 16px; border-bottom: 1px solid var(--border); font-size: 14px; vertical-align: top; }}
861+ tr:last-child td {{ border-bottom: none; }}
862+ .badge {{ display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: 600; color: white;}}
863+ .font-mono {{ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; }}
864+ footer {{ text-align: center; padding: 40px; color: var(--secondary); font-size: 13px; }}
865+ </style>
866+ </head>
867+ <body>
868+ <div class="header">
869+ <div class="header-content">
870+ <div class="logo">
871+ <h1>Cryptography Scan Report</h1>
872+ <p>Target: { domain } </p>
873+ </div>
874+ <div class="scan-meta">
875+ <div>{ current_datetime } </div>
876+ </div>
877+ </div>
878+ </div>
879+ <div class="container">
880+ { get_crypto_html_snippets (report_data )}
881+ <footer>
882+ <p>Generated by Zeuz Security Automation Framework • { current_datetime } </p>
883+ </footer>
884+ </div>
885+ </body>
886+ </html>
887+ """
888+
889+ if output_dir :
890+ os .makedirs (output_dir , exist_ok = True )
891+ else :
892+ output_dir = '.'
893+ out_path = os .path .join (output_dir , f"crypto_report_{ domain .replace ('.' , '_' )} .html" )
894+ with open (out_path , "w" , encoding = "utf-8" ) as f :
895+ f .write (html_template )
896+ print (f"Enhanced cryptography report saved: { out_path } " )
897+ return out_path
898+
899+
900+ def cryptography_scan_run (url , security_report_dir = None ):
901+ if hasattr (security_report_dir , 'mkdir' ):
902+ security_report_dir .mkdir (parents = True , exist_ok = True )
903+ elif security_report_dir :
904+ os .makedirs (security_report_dir , exist_ok = True )
905+
906+ print (f"Running Cryptography scan for { url } ..." )
907+
908+ def get_cryptography_data (url ):
909+ if not url .startswith ("http" ):
910+ url = "https://" + url
911+
912+ parsed = urllib .parse .urlparse (url )
913+ domain = parsed .netloc .split (':' )[0 ]
914+
915+ report_data = {
916+ "domain" : domain ,
917+ "url" : url ,
918+ "ssl" : {"valid" : False , "expired" : False , "self_signed" : False , "wrong_hostname" : False , "tls_versions" : []},
919+ "headers" : {},
920+ "banner" : {},
921+ "whois" : {},
922+ "geo" : {}
923+ }
924+ # 1. SSL Certificate Info
925+ print (f"[{ domain } ] Fetching SSL/TLS Certificate information..." , flush = True )
926+ try :
927+ context = ssl .create_default_context ()
928+ try :
929+ with socket .create_connection ((domain , 443 ), timeout = 2 ) as sock :
930+ with context .wrap_socket (sock , server_hostname = domain ) as ssock :
931+ cert = ssock .getpeercert ()
932+ report_data ['ssl' ]['expired' ] = ssl .cert_time_to_seconds (cert ['notAfter' ]) < time .time ()
933+
934+ issuer = dict (x [0 ] for x in cert .get ('issuer' , []))
935+ subject = dict (x [0 ] for x in cert .get ('subject' , []))
936+ report_data ['ssl' ]['self_signed' ] = issuer == subject
937+
938+ report_data ['ssl' ]['valid' ] = True
939+ report_data ['ssl' ]['not_after' ] = cert ['notAfter' ]
940+ report_data ['ssl' ]['issuer' ] = issuer .get ('organizationName' , issuer .get ('commonName' , 'Unknown' ))
941+
942+ # Check TLS Version while socket is open
943+ ver = ssock .version ()
944+ if ver :
945+ strength = 'Good' if ver in ['TLSv1.2' , 'TLSv1.3' ] else 'Weak'
946+ report_data ['ssl' ]['tls_versions' ] = [(ver , strength )]
947+
948+ except ssl .CertificateError as e :
949+ report_data ['ssl' ]['wrong_hostname' ] = True
950+ report_data ['ssl' ]['valid' ] = False
951+ report_data ['ssl' ]['error' ] = str (e )
952+
953+ # Reconnect without verification to get cert info anyway
954+ ctx_no_verify = ssl ._create_unverified_context ()
955+ with socket .create_connection ((domain , 443 ), timeout = 2 ) as sock :
956+ with ctx_no_verify .wrap_socket (sock , server_hostname = domain ) as ssock :
957+ cert = ssock .getpeercert ()
958+ if cert :
959+ report_data ['ssl' ]['expired' ] = ssl .cert_time_to_seconds (cert ['notAfter' ]) < time .time ()
960+ issuer = dict (x [0 ] for x in cert .get ('issuer' , []))
961+ report_data ['ssl' ]['not_after' ] = cert ['notAfter' ]
962+ report_data ['ssl' ]['issuer' ] = issuer .get ('organizationName' , issuer .get ('commonName' , 'Unknown' ))
963+ ver = ssock .version ()
964+ if ver :
965+ strength = 'Good' if ver in ['TLSv1.2' , 'TLSv1.3' ] else 'Weak'
966+ report_data ['ssl' ]['tls_versions' ] = [(ver , strength )]
967+ except Exception as e :
968+ report_data ['ssl' ]['valid' ] = 'Error'
969+ report_data ['ssl' ]['error' ] = str (e )
970+ if '[Errno 54]' in str (e ) or 'timed out' in str (e ).lower () or 'Connection refused' in str (e ):
971+ report_data ['ssl' ]['blocked' ] = True
972+ print (f"[{ domain } ] WARNING: Access Blocked/Reset by Domain Firewall/WAF." , flush = True )
973+
974+ # 2. HTTP Headers & Server Banner
975+ print (f"[{ domain } ] Fetching HTTP Security Headers..." , flush = True )
976+ try :
977+ ctx = ssl ._create_unverified_context ()
978+ req = urllib .request .Request (url , headers = {'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' })
979+ with urllib .request .urlopen (req , context = ctx , timeout = 2 ) as resp :
980+ headers = resp .headers
981+ except urllib .error .HTTPError as e :
982+ headers = e .headers
983+ except Exception as e :
984+ headers = {}
985+ report_data ['headers' ]['error' ] = str (e )
986+
987+ if headers :
988+ report_data ['headers' ] = {
989+ "Strict-Transport-Security" : headers .get ("Strict-Transport-Security" , "Missing" ),
990+ "Content-Security-Policy" : headers .get ("Content-Security-Policy" , "Missing" ),
991+ "X-Content-Type-Options" : headers .get ("X-Content-Type-Options" , "Missing" )
992+ }
993+ report_data ['banner' ] = {
994+ "Server" : headers .get ("Server" , "Hidden" ),
995+ "X-Powered-By" : headers .get ("X-Powered-By" , headers .get ("X-Powered-By-Plesk" , "Hidden" ))
996+ }
997+
998+ # 3. IP Geolocation ONLY
999+ print (f"[{ domain } ] Fetching IP Geolocation..." , flush = True )
1000+ try :
1001+ ip = socket .gethostbyname (domain )
1002+ geo_req = urllib .request .Request (f"http://ip-api.com/json/{ ip } " )
1003+ with urllib .request .urlopen (geo_req , timeout = 2 ) as geo_resp :
1004+ geo_data = json .loads (geo_resp .read ().decode ())
1005+ report_data ['geo' ] = {
1006+ "IP" : ip ,
1007+ "Country" : geo_data .get ("country" , "Unknown" ),
1008+ "Hosting Provider" : geo_data .get ("isp" , geo_data .get ("org" , "Unknown" ))
1009+ }
1010+ except Exception as e :
1011+ report_data ['geo' ] = {"IP" : socket .gethostbyname (domain ) if 'domain' in locals () else 'Unknown' }
1012+
1013+ return report_data
1014+
1015+
1016+ def cryptography_scan_run (url , security_report_dir = None ):
1017+ if hasattr (security_report_dir , 'mkdir' ):
1018+ security_report_dir .mkdir (parents = True , exist_ok = True )
1019+ elif security_report_dir :
1020+ os .makedirs (security_report_dir , exist_ok = True )
1021+
1022+ print (f"Running Cryptography scan for { url } ..." )
1023+
1024+ report_data = get_cryptography_data (url )
1025+ domain = report_data .get ("domain" , url )
1026+
1027+ html_result = generate_crypto_html (report_data , domain , security_report_dir )
1028+ print ("Cryptography scan complete!" )
1029+ return {
1030+ "report_data" : report_data ,
6951031 "html" : html_result
6961032 }
0 commit comments