scanner.py

Download
python 729 lines 25.8 KB
  1#!/usr/bin/env python3
  2"""
  3λ„€νŠΈμ›Œν¬ λ³΄μ•ˆ μŠ€μΊλ„ˆ (ꡐ윑용)
  4Network Security Scanner (Educational)
  5
  6포트 μŠ€μΊλ‹, HTTP λ³΄μ•ˆ 헀더 검사, SSL/TLS 정보 μˆ˜μ§‘, λ°°λ„ˆ κ·Έλž˜λΉ™,
  7리포트 생성을 ν¬ν•¨ν•˜λŠ” ꡐ윑용 λ„€νŠΈμ›Œν¬ λ³΄μ•ˆ μŠ€μΊλ„ˆμž…λ‹ˆλ‹€.
  8λͺ¨λ“  μŠ€μΊ”μ— 레이트 λ¦¬λ―ΈνŒ…μ΄ μ μš©λ©λ‹ˆλ‹€.
  9
 10Implements an educational network security scanner with TCP port scanning,
 11HTTP security header checking, SSL/TLS info gathering, banner grabbing,
 12and report generation. Rate limiting is applied between all connections.
 13
 14============================================================================
 15ETHICAL USE DISCLAIMER / 윀리적 μ‚¬μš© κ³ μ§€
 16============================================================================
 17
 18This tool is provided STRICTLY for EDUCATIONAL purposes only.
 19
 20- ONLY scan systems you OWN or have EXPLICIT WRITTEN PERMISSION to test.
 21- Unauthorized port scanning may violate laws (CFAA, Computer Misuse Act, etc.)
 22- The authors assume NO LIABILITY for misuse of this tool.
 23- When in doubt, scan localhost (127.0.0.1) only.
 24
 25이 λ„κ΅¬λŠ” 였직 ꡐ윑 λͺ©μ μœΌλ‘œλ§Œ μ œκ³΅λ©λ‹ˆλ‹€.
 26- 본인이 μ†Œμœ ν•˜κ±°λ‚˜ λͺ…μ‹œμ  μ„œλ©΄ ν—ˆκ°€λ₯Ό 받은 μ‹œμŠ€ν…œλ§Œ μŠ€μΊ”ν•˜μ„Έμš”.
 27- 무단 포트 μŠ€μΊλ‹μ€ 법λ₯  μœ„λ°˜μ΄ 될 수 μžˆμŠ΅λ‹ˆλ‹€.
 28============================================================================
 29"""
 30
 31import argparse
 32import json
 33import socket
 34import ssl
 35import sys
 36import time
 37import re
 38from datetime import datetime, timezone
 39from urllib.parse import urlparse
 40from http.client import HTTPSConnection, HTTPConnection
 41
 42
 43# =============================================================================
 44# Configuration (μ„€μ •)
 45# =============================================================================
 46
 47DEFAULT_PORTS = [21, 22, 23, 25, 53, 80, 110, 143, 443, 993, 995,
 48                 3306, 5432, 6379, 8080, 8443, 27017]
 49
 50WELL_KNOWN_SERVICES = {
 51    21: "FTP", 22: "SSH", 23: "Telnet", 25: "SMTP", 53: "DNS",
 52    80: "HTTP", 110: "POP3", 143: "IMAP", 443: "HTTPS",
 53    993: "IMAPS", 995: "POP3S", 3306: "MySQL", 5432: "PostgreSQL",
 54    6379: "Redis", 8080: "HTTP-Alt", 8443: "HTTPS-Alt", 27017: "MongoDB",
 55}
 56
 57# Security headers that should be present
 58RECOMMENDED_HEADERS = {
 59    "Strict-Transport-Security": {
 60        "description": "Enforces HTTPS connections",
 61        "severity": "HIGH",
 62    },
 63    "Content-Security-Policy": {
 64        "description": "Prevents XSS and data injection attacks",
 65        "severity": "HIGH",
 66    },
 67    "X-Content-Type-Options": {
 68        "description": "Prevents MIME type sniffing",
 69        "severity": "MEDIUM",
 70        "expected": "nosniff",
 71    },
 72    "X-Frame-Options": {
 73        "description": "Prevents clickjacking attacks",
 74        "severity": "MEDIUM",
 75        "expected_any": ["DENY", "SAMEORIGIN"],
 76    },
 77    "Referrer-Policy": {
 78        "description": "Controls referrer information leakage",
 79        "severity": "LOW",
 80    },
 81    "Permissions-Policy": {
 82        "description": "Controls browser feature permissions",
 83        "severity": "LOW",
 84    },
 85    "X-XSS-Protection": {
 86        "description": "Legacy XSS filter (use CSP instead)",
 87        "severity": "INFO",
 88        "note": "Modern recommendation: set to '0' and rely on CSP",
 89    },
 90}
 91
 92
 93# =============================================================================
 94# 1. Port Scanner (포트 μŠ€μΊλ„ˆ)
 95# =============================================================================
 96
 97class PortScanner:
 98    """
 99    TCP connect scanner for educational purposes.
100    Uses full TCP handshake (connect scan), not SYN scan.
101    """
102
103    def __init__(self, target: str, ports: list[int], timeout: float = 1.0,
104                 delay: float = 0.1):
105        self.target = target
106        self.ports = ports
107        self.timeout = timeout
108        self.delay = delay  # Rate limiting delay between connections
109        self.results: list[dict] = []
110
111    def scan(self) -> list[dict]:
112        """Scan all configured ports with rate limiting."""
113        print(f"\n  Scanning {self.target} ({len(self.ports)} ports)...")
114        print(f"  Timeout: {self.timeout}s | Delay: {self.delay}s between probes\n")
115
116        self.results = []
117        for i, port in enumerate(self.ports):
118            result = self._scan_port(port)
119            self.results.append(result)
120
121            if result["state"] == "open":
122                service = WELL_KNOWN_SERVICES.get(port, "unknown")
123                banner = result.get("banner", "")
124                banner_str = f" | Banner: {banner}" if banner else ""
125                print(f"    Port {port:5d}/tcp  OPEN    ({service}){banner_str}")
126
127            # Rate limiting
128            if i < len(self.ports) - 1:
129                time.sleep(self.delay)
130
131        open_count = sum(1 for r in self.results if r["state"] == "open")
132        print(f"\n  Scan complete: {open_count} open / {len(self.ports)} scanned")
133        return self.results
134
135    def _scan_port(self, port: int) -> dict:
136        """Attempt TCP connection to a single port."""
137        result = {
138            "port": port,
139            "service": WELL_KNOWN_SERVICES.get(port, "unknown"),
140            "state": "closed",
141            "banner": "",
142        }
143
144        try:
145            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
146            sock.settimeout(self.timeout)
147            error = sock.connect_ex((self.target, port))
148
149            if error == 0:
150                result["state"] = "open"
151                # Attempt banner grab
152                result["banner"] = self._grab_banner(sock, port)
153            sock.close()
154        except socket.timeout:
155            result["state"] = "filtered"
156        except OSError:
157            result["state"] = "closed"
158
159        return result
160
161    def _grab_banner(self, sock: socket.socket, port: int) -> str:
162        """Attempt to read a service banner from an open port."""
163        try:
164            # Some services send banner on connect, others need a nudge
165            if port in (80, 8080):
166                # HTTP: send a minimal request
167                sock.sendall(b"HEAD / HTTP/1.0\r\nHost: localhost\r\n\r\n")
168            elif port in (25, 110, 143):
169                pass  # SMTP/POP3/IMAP send banner automatically
170
171            sock.settimeout(0.5)
172            banner = sock.recv(256)
173            # Clean up banner: remove non-printable chars
174            cleaned = re.sub(r'[^\x20-\x7e]', '', banner.decode("utf-8", errors="replace"))
175            return cleaned.strip()[:100]
176        except (socket.timeout, OSError, UnicodeDecodeError):
177            return ""
178
179
180# =============================================================================
181# 2. HTTP Security Header Checker (HTTP λ³΄μ•ˆ 헀더 검사)
182# =============================================================================
183
184class HeaderChecker:
185    """Check HTTP response headers against security best practices."""
186
187    def __init__(self, target: str):
188        self.target = target
189        self.response_headers: dict = {}
190        self.findings: list[dict] = []
191
192    def check(self) -> list[dict]:
193        """Fetch headers and analyze them."""
194        print(f"\n  Checking HTTP security headers for: {self.target}")
195
196        self.response_headers = self._fetch_headers()
197        if not self.response_headers:
198            print("  ERROR: Could not connect to target")
199            return []
200
201        print(f"\n  Response Headers Received:")
202        for key, value in sorted(self.response_headers.items()):
203            print(f"    {key}: {value[:60]}")
204
205        self.findings = self._analyze_headers()
206
207        print(f"\n  Security Header Analysis:")
208        for finding in self.findings:
209            symbol = "OK" if finding["status"] == "present" else "!!"
210            print(f"    [{symbol}] [{finding['severity']:6s}] {finding['header']}")
211            print(f"          {finding['message']}")
212
213        present = sum(1 for f in self.findings if f["status"] == "present")
214        total = len(self.findings)
215        print(f"\n  Score: {present}/{total} recommended headers present")
216        return self.findings
217
218    def _fetch_headers(self) -> dict:
219        """Fetch HTTP(S) headers from target."""
220        try:
221            parsed = urlparse(self.target if "://" in self.target else f"https://{self.target}")
222            host = parsed.hostname or self.target
223            port = parsed.port
224            path = parsed.path or "/"
225
226            if parsed.scheme == "https" or (port and port == 443):
227                conn = HTTPSConnection(host, port=port or 443, timeout=5)
228            else:
229                conn = HTTPConnection(host, port=port or 80, timeout=5)
230
231            conn.request("HEAD", path)
232            response = conn.getresponse()
233            headers = dict(response.getheaders())
234            conn.close()
235            return headers
236        except Exception as e:
237            print(f"  Connection error: {e}")
238            return {}
239
240    def _analyze_headers(self) -> list[dict]:
241        """Compare response headers against recommendations."""
242        findings = []
243        # Normalize header keys to title case for comparison
244        norm_headers = {k.lower(): v for k, v in self.response_headers.items()}
245
246        for header, info in RECOMMENDED_HEADERS.items():
247            header_lower = header.lower()
248            finding = {
249                "header": header,
250                "severity": info["severity"],
251                "description": info["description"],
252            }
253
254            if header_lower in norm_headers:
255                value = norm_headers[header_lower]
256                finding["status"] = "present"
257                finding["value"] = value
258                finding["message"] = f"Present: {value[:50]}"
259
260                # Check expected value
261                if "expected" in info and info["expected"].lower() != value.lower():
262                    finding["message"] += f" (recommended: {info['expected']})"
263                if "expected_any" in info:
264                    if not any(v.lower() in value.lower() for v in info["expected_any"]):
265                        finding["message"] += f" (recommended: {' or '.join(info['expected_any'])})"
266            else:
267                finding["status"] = "missing"
268                finding["message"] = f"MISSING - {info['description']}"
269
270            findings.append(finding)
271
272        # Check for information disclosure headers
273        disclosure_headers = ["server", "x-powered-by", "x-aspnet-version"]
274        for h in disclosure_headers:
275            if h in norm_headers:
276                findings.append({
277                    "header": h.title(),
278                    "severity": "LOW",
279                    "status": "warning",
280                    "value": norm_headers[h],
281                    "message": f"Information disclosure: {norm_headers[h]} (consider removing)",
282                    "description": "Reveals server technology information",
283                })
284
285        return findings
286
287
288# =============================================================================
289# 3. SSL/TLS Info Gatherer (SSL/TLS 정보 μˆ˜μ§‘)
290# =============================================================================
291
292class TLSChecker:
293    """Gather SSL/TLS certificate and protocol information."""
294
295    def __init__(self, target: str, port: int = 443):
296        self.target = target
297        self.port = port
298        self.cert_info: dict = {}
299
300    def check(self) -> dict:
301        """Connect and gather TLS information."""
302        print(f"\n  Checking SSL/TLS for: {self.target}:{self.port}")
303
304        try:
305            context = ssl.create_default_context()
306            with socket.create_connection((self.target, self.port), timeout=5) as sock:
307                with context.wrap_socket(sock, server_hostname=self.target) as ssock:
308                    self.cert_info = self._extract_info(ssock)
309                    self._print_info()
310                    return self.cert_info
311        except ssl.SSLCertVerificationError as e:
312            print(f"  SSL Certificate Error: {e}")
313            self.cert_info = {"error": str(e), "verified": False}
314        except (socket.timeout, OSError) as e:
315            print(f"  Connection Error: {e}")
316            self.cert_info = {"error": str(e)}
317
318        return self.cert_info
319
320    def _extract_info(self, ssock: ssl.SSLSocket) -> dict:
321        """Extract TLS session and certificate details."""
322        cert = ssock.getpeercert()
323        cipher = ssock.cipher()
324
325        info = {
326            "protocol": ssock.version(),
327            "cipher_suite": cipher[0] if cipher else "unknown",
328            "cipher_bits": cipher[2] if cipher else 0,
329            "verified": True,
330            "subject": dict(x[0] for x in cert.get("subject", ())),
331            "issuer": dict(x[0] for x in cert.get("issuer", ())),
332            "serial": cert.get("serialNumber", ""),
333            "not_before": cert.get("notBefore", ""),
334            "not_after": cert.get("notAfter", ""),
335            "san": [entry[1] for entry in cert.get("subjectAltName", ())],
336        }
337
338        # Check expiry
339        if info["not_after"]:
340            try:
341                expiry = datetime.strptime(info["not_after"], "%b %d %H:%M:%S %Y %Z")
342                days_left = (expiry - datetime.utcnow()).days
343                info["days_until_expiry"] = days_left
344                info["expired"] = days_left < 0
345            except ValueError:
346                pass
347
348        return info
349
350    def _print_info(self):
351        """Print TLS information in a readable format."""
352        info = self.cert_info
353        print(f"\n  TLS Connection:")
354        print(f"    Protocol:     {info.get('protocol', 'N/A')}")
355        print(f"    Cipher:       {info.get('cipher_suite', 'N/A')}")
356        print(f"    Key bits:     {info.get('cipher_bits', 'N/A')}")
357        print(f"    Verified:     {info.get('verified', False)}")
358
359        print(f"\n  Certificate:")
360        subject = info.get("subject", {})
361        print(f"    Subject:      {subject.get('commonName', 'N/A')}")
362        issuer = info.get("issuer", {})
363        print(f"    Issuer:       {issuer.get('organizationName', 'N/A')}")
364        print(f"    Valid from:   {info.get('not_before', 'N/A')}")
365        print(f"    Valid until:  {info.get('not_after', 'N/A')}")
366
367        days = info.get("days_until_expiry")
368        if days is not None:
369            status = "EXPIRED" if days < 0 else f"{days} days remaining"
370            if 0 < days <= 30:
371                status += " (WARNING: expiring soon)"
372            print(f"    Expiry:       {status}")
373
374        san = info.get("san", [])
375        if san:
376            print(f"    SAN entries:  {', '.join(san[:5])}")
377            if len(san) > 5:
378                print(f"                  ... and {len(san) - 5} more")
379
380
381# =============================================================================
382# 4. Report Generator (리포트 생성기)
383# =============================================================================
384
385class ReportGenerator:
386    """Generate a structured security scan report."""
387
388    def __init__(self, target: str):
389        self.target = target
390        self.timestamp = datetime.now(timezone.utc).isoformat()
391        self.sections: list[dict] = []
392
393    def add_port_scan(self, results: list[dict]):
394        self.sections.append({"type": "port_scan", "data": results})
395
396    def add_header_check(self, findings: list[dict]):
397        self.sections.append({"type": "header_check", "data": findings})
398
399    def add_tls_check(self, info: dict):
400        self.sections.append({"type": "tls_check", "data": info})
401
402    def generate_text(self) -> str:
403        """Generate a plain-text report."""
404        lines = [
405            "=" * 70,
406            "  SECURITY SCAN REPORT",
407            "=" * 70,
408            f"  Target:    {self.target}",
409            f"  Timestamp: {self.timestamp}",
410            f"  Sections:  {len(self.sections)}",
411            "",
412        ]
413
414        for section in self.sections:
415            if section["type"] == "port_scan":
416                lines.append("-" * 70)
417                lines.append("  PORT SCAN RESULTS")
418                lines.append("-" * 70)
419                open_ports = [r for r in section["data"] if r["state"] == "open"]
420                for r in open_ports:
421                    banner = f"  Banner: {r['banner']}" if r["banner"] else ""
422                    lines.append(f"    {r['port']:5d}/tcp  {r['state']:8s}  {r['service']}{banner}")
423                lines.append(f"\n    Open: {len(open_ports)} / Scanned: {len(section['data'])}")
424                lines.append("")
425
426            elif section["type"] == "header_check":
427                lines.append("-" * 70)
428                lines.append("  HTTP SECURITY HEADERS")
429                lines.append("-" * 70)
430                for f in section["data"]:
431                    symbol = "OK" if f["status"] == "present" else "!!"
432                    lines.append(f"    [{symbol}] {f['header']:35s}  {f['message'][:50]}")
433                lines.append("")
434
435            elif section["type"] == "tls_check":
436                lines.append("-" * 70)
437                lines.append("  TLS/SSL INFORMATION")
438                lines.append("-" * 70)
439                data = section["data"]
440                if "error" in data:
441                    lines.append(f"    Error: {data['error']}")
442                else:
443                    lines.append(f"    Protocol:  {data.get('protocol', 'N/A')}")
444                    lines.append(f"    Cipher:    {data.get('cipher_suite', 'N/A')}")
445                    subj = data.get('subject', {})
446                    lines.append(f"    Subject:   {subj.get('commonName', 'N/A')}")
447                    days = data.get('days_until_expiry')
448                    if days is not None:
449                        lines.append(f"    Expiry:    {days} days remaining")
450                lines.append("")
451
452        lines.append("=" * 70)
453        lines.append("  END OF REPORT")
454        lines.append("=" * 70)
455
456        return "\n".join(lines)
457
458    def generate_json(self) -> str:
459        """Generate a JSON report."""
460        report = {
461            "target": self.target,
462            "timestamp": self.timestamp,
463            "sections": self.sections,
464        }
465        return json.dumps(report, indent=2, default=str)
466
467
468# =============================================================================
469# 5. CLI Interface (CLI μΈν„°νŽ˜μ΄μŠ€)
470# =============================================================================
471
472def parse_port_range(port_str: str) -> list[int]:
473    """Parse port specification: '80', '80,443', '1-1024', 'common'."""
474    if port_str.lower() == "common":
475        return DEFAULT_PORTS
476
477    ports = set()
478    for part in port_str.split(","):
479        part = part.strip()
480        if "-" in part:
481            start, end = part.split("-", 1)
482            start, end = int(start), int(end)
483            if 1 <= start <= end <= 65535:
484                ports.update(range(start, end + 1))
485        else:
486            p = int(part)
487            if 1 <= p <= 65535:
488                ports.add(p)
489    return sorted(ports)
490
491
492def run_scan(args):
493    """Execute the scan based on CLI arguments."""
494    target = args.target
495    report = ReportGenerator(target)
496
497    # Resolve hostname
498    try:
499        ip = socket.gethostbyname(target)
500        print(f"\n  Target: {target} ({ip})")
501    except socket.gaierror:
502        print(f"\n  ERROR: Cannot resolve hostname: {target}")
503        return
504
505    # Port scan
506    if not args.skip_ports:
507        ports = parse_port_range(args.ports)
508        scanner = PortScanner(target, ports, timeout=args.timeout, delay=args.delay)
509        results = scanner.scan()
510        report.add_port_scan(results)
511
512    # HTTP header check
513    if not args.skip_headers:
514        checker = HeaderChecker(target)
515        findings = checker.check()
516        if findings:
517            report.add_header_check(findings)
518
519    # TLS check
520    if not args.skip_tls:
521        tls = TLSChecker(target, port=args.tls_port)
522        info = tls.check()
523        if info:
524            report.add_tls_check(info)
525
526    # Generate report
527    if args.output:
528        if args.format == "json":
529            report_text = report.generate_json()
530        else:
531            report_text = report.generate_text()
532
533        with open(args.output, "w") as f:
534            f.write(report_text)
535        print(f"\n  Report saved to: {args.output}")
536    else:
537        print("\n" + report.generate_text())
538
539
540def run_demo():
541    """Run a localhost-only demo when no arguments are provided."""
542    print("=" * 60)
543    print("  Network Security Scanner - Demo Mode")
544    print("  λ„€νŠΈμ›Œν¬ λ³΄μ•ˆ μŠ€μΊλ„ˆ - 데λͺ¨ λͺ¨λ“œ")
545    print("=" * 60)
546
547    print("""
548  ETHICAL USE DISCLAIMER:
549  This tool is for EDUCATIONAL purposes only.
550  Only scan systems you own or have written permission to test.
551  Unauthorized scanning may violate applicable laws.
552""")
553
554    # Demo 1: Port scanner on localhost (most ports will be closed)
555    print("-" * 60)
556    print("  Demo 1: Port Scan (localhost, limited ports)")
557    print("-" * 60)
558
559    scanner = PortScanner(
560        "127.0.0.1",
561        [22, 80, 443, 3000, 5000, 5050, 8080, 8443],
562        timeout=0.3,
563        delay=0.05,
564    )
565    port_results = scanner.scan()
566
567    # Demo 2: Show header check logic (without real connection)
568    print("\n" + "-" * 60)
569    print("  Demo 2: HTTP Security Header Analysis (simulated)")
570    print("-" * 60)
571
572    # Simulate response headers from a typical website
573    simulated_headers = {
574        "Content-Type": "text/html; charset=utf-8",
575        "Strict-Transport-Security": "max-age=31536000; includeSubDomains",
576        "X-Content-Type-Options": "nosniff",
577        "X-Frame-Options": "SAMEORIGIN",
578        "Server": "nginx/1.24.0",
579    }
580
581    print(f"\n  Simulated response headers:")
582    for k, v in simulated_headers.items():
583        print(f"    {k}: {v}")
584
585    print(f"\n  Analysis:")
586    norm = {k.lower(): v for k, v in simulated_headers.items()}
587    for header, info in RECOMMENDED_HEADERS.items():
588        h_lower = header.lower()
589        if h_lower in norm:
590            print(f"    [OK] {header}: {norm[h_lower][:50]}")
591        else:
592            print(f"    [!!] {header}: MISSING - {info['description']}")
593
594    # Check info disclosure
595    if "server" in norm:
596        print(f"    [!!] Server header reveals: {norm['server']} (consider removing)")
597
598    # Demo 3: Show TLS check info
599    print(f"\n" + "-" * 60)
600    print("  Demo 3: SSL/TLS Check (simulated)")
601    print("-" * 60)
602
603    simulated_tls = {
604        "protocol": "TLSv1.3",
605        "cipher_suite": "TLS_AES_256_GCM_SHA384",
606        "cipher_bits": 256,
607        "verified": True,
608        "subject": {"commonName": "example.com"},
609        "issuer": {"organizationName": "Let's Encrypt"},
610        "days_until_expiry": 45,
611    }
612
613    print(f"\n  Simulated TLS info:")
614    print(f"    Protocol:  {simulated_tls['protocol']}")
615    print(f"    Cipher:    {simulated_tls['cipher_suite']}")
616    print(f"    Key bits:  {simulated_tls['cipher_bits']}")
617    print(f"    Subject:   {simulated_tls['subject']['commonName']}")
618    print(f"    Issuer:    {simulated_tls['issuer']['organizationName']}")
619    print(f"    Expiry:    {simulated_tls['days_until_expiry']} days remaining")
620
621    # Demo 4: Report generation
622    print(f"\n" + "-" * 60)
623    print("  Demo 4: Report Generation")
624    print("-" * 60)
625
626    report = ReportGenerator("127.0.0.1")
627    report.add_port_scan(port_results)
628    print(f"\n{report.generate_text()}")
629
630    # CLI usage info
631    print("\n" + "-" * 60)
632    print("  CLI Usage Examples:")
633    print("-" * 60)
634    print("""
635  # Scan localhost (safe for testing)
636  python scanner.py 127.0.0.1
637
638  # Scan common ports on your own server
639  python scanner.py myserver.com --ports common
640
641  # Scan specific port range with custom timeout
642  python scanner.py myserver.com --ports 80,443,8080 --timeout 2.0
643
644  # Full scan with JSON report output
645  python scanner.py myserver.com --ports 1-1024 --output report.json --format json
646
647  # Skip port scan, only check headers and TLS
648  python scanner.py myserver.com --skip-ports
649
650  # Skip TLS check
651  python scanner.py myserver.com --skip-tls
652""")
653
654
655def build_parser() -> argparse.ArgumentParser:
656    """Build the CLI argument parser."""
657    parser = argparse.ArgumentParser(
658        description="Educational Network Security Scanner",
659        epilog="DISCLAIMER: Only scan systems you own or have permission to test.",
660    )
661    parser.add_argument(
662        "target", nargs="?", default=None,
663        help="Target hostname or IP address (default: run demo)",
664    )
665    parser.add_argument(
666        "--ports", "-p", default="common",
667        help="Ports to scan: '80,443', '1-1024', or 'common' (default: common)",
668    )
669    parser.add_argument(
670        "--timeout", "-t", type=float, default=1.0,
671        help="Connection timeout in seconds (default: 1.0)",
672    )
673    parser.add_argument(
674        "--delay", "-d", type=float, default=0.1,
675        help="Delay between port probes in seconds (default: 0.1)",
676    )
677    parser.add_argument(
678        "--tls-port", type=int, default=443,
679        help="Port for TLS check (default: 443)",
680    )
681    parser.add_argument(
682        "--skip-ports", action="store_true",
683        help="Skip port scanning",
684    )
685    parser.add_argument(
686        "--skip-headers", action="store_true",
687        help="Skip HTTP header check",
688    )
689    parser.add_argument(
690        "--skip-tls", action="store_true",
691        help="Skip TLS/SSL check",
692    )
693    parser.add_argument(
694        "--output", "-o",
695        help="Output report to file",
696    )
697    parser.add_argument(
698        "--format", "-f", choices=["text", "json"], default="text",
699        help="Report format (default: text)",
700    )
701    return parser
702
703
704# =============================================================================
705# Main
706# =============================================================================
707
708if __name__ == "__main__":
709    parser = build_parser()
710    args = parser.parse_args()
711
712    print("\n" + "=" * 60)
713    print("  Network Security Scanner (Educational)")
714    print("  λ„€νŠΈμ›Œν¬ λ³΄μ•ˆ μŠ€μΊλ„ˆ (ꡐ윑용)")
715    print("=" * 60)
716
717    if args.target is None:
718        run_demo()
719    else:
720        print("""
721  DISCLAIMER: This tool is for educational purposes only.
722  Only scan systems you own or have written permission to test.
723""")
724        run_scan(args)
725
726    print("\n" + "=" * 60)
727    print("  Scan complete.")
728    print("=" * 60)