is-it-safe 5.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mithun
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: is-it-safe
3
+ Version: 5.0.0
4
+ Summary: Stealthy Security Layer Detector (WAF/IDS/IPS/Fail2Ban)
5
+ Author-email: Mithun <mitchastertheblaster@gmail.com>
6
+ Project-URL: Homepage, https://github.com/mithun/is-it-safe
7
+ Project-URL: Bug Tracker, https://github.com/mithun/is-it-safe/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Topic :: Security
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: requests>=2.31.0
16
+ Requires-Dist: urllib3>=2.0.0
17
+ Requires-Dist: rich>=13.7.0
18
+ Requires-Dist: ipaddress>=1.0.23
19
+ Requires-Dist: scapy>=2.5.0
20
+ Requires-Dist: paramiko>=3.4.0
21
+ Provides-Extra: test
22
+ Requires-Dist: pytest>=7.4.0; extra == "test"
23
+ Requires-Dist: pytest-mock>=3.12.0; extra == "test"
24
+ Dynamic: license-file
25
+
26
+ # is-it-safe 🛡️
27
+
28
+ [![PyPI version](https://img.shields.io/pypi/v/is-it-safe.svg)](https://pypi.org/project/is-it-safe/)
29
+ [![Python versions](https://img.shields.io/pypi/pyversions/is-it-safe.svg)](https://pypi.org/project/is-it-safe/)
30
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
31
+
32
+ **Stealthy Security Layer Fingerprinting & Detection v5.0**
33
+
34
+ `is-it-safe` is a modern, high-performance security utility designed to map and identify protective layers surrounding a target without triggering aggressive defense mechanisms. It provides deep visibility into infrastructure security by fingerprinting WAFs, IDS/IPS, and automated blocking systems.
35
+
36
+ ## Key Features
37
+
38
+ * 🛡️ **WAF Fingerprinting:** Identifies 10+ major WAF vendors (Cloudflare, Akamai, AWS, Imperva, etc.) via signature-based and behavioral analysis.
39
+ * 🕵️ **Stealth-First Detection:** Implements adaptive jitter, randomized headers, and low-signal request patterns to bypass basic rate-limiters and heuristics.
40
+ * 🚦 **IDS/IPS Probing:** Uses low-level TCP signals and HTTP response anomalies to detect deep packet inspection and network-level interception.
41
+ * 🚫 **Fail2Ban Discovery:** Safely identifies SSH tarpits, "honey-pots," and active ban policies through non-destructive authentication probing.
42
+ * 🎨 **Modern Interface:** Built with `rich` for professional, structured terminal output and high-visibility results.
43
+ * 🤖 **Automation Ready:** Native JSON output mode for seamless integration into larger security pipelines.
44
+
45
+ ## Installation
46
+
47
+ ### The Modern Way (Recommended)
48
+ Use [uv](https://github.com/astral-sh/uv) for the fastest experience:
49
+
50
+ ```bash
51
+ # Run instantly without installing
52
+ uvx is-it-safe example.com
53
+
54
+ # Or install it
55
+ uv pip install is-it-safe
56
+ ```
57
+
58
+ ### The Traditional Way
59
+ ```bash
60
+ pip install is-it-safe
61
+ ```
62
+
63
+ ### From Source
64
+ ```bash
65
+ git clone https://github.com/your-username/is-it-safe.git
66
+ cd is-it-safe
67
+ pip install .
68
+ ```
69
+
70
+ ## 🛠 Usage
71
+
72
+ ```bash
73
+ # Basic scan
74
+ is-it-safe example.com
75
+
76
+ # Verbose scan with stealth enabled
77
+ is-it-safe example.com --stealth --verbose
78
+
79
+ # Scan specific SSH port for Fail2Ban
80
+ sudo is-it-safe example.com --ssh-port 2222
81
+
82
+ # Output results as JSON
83
+ is-it-safe example.com --json > results.json
84
+ ```
85
+
86
+ > [!IMPORTANT]
87
+ > Some IDS/IPS detection features require **root privileges** for raw socket access.
88
+
89
+ ## 📜 License
90
+
91
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
92
+
@@ -0,0 +1,67 @@
1
+ # is-it-safe 🛡️
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/is-it-safe.svg)](https://pypi.org/project/is-it-safe/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/is-it-safe.svg)](https://pypi.org/project/is-it-safe/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ **Stealthy Security Layer Fingerprinting & Detection v5.0**
8
+
9
+ `is-it-safe` is a modern, high-performance security utility designed to map and identify protective layers surrounding a target without triggering aggressive defense mechanisms. It provides deep visibility into infrastructure security by fingerprinting WAFs, IDS/IPS, and automated blocking systems.
10
+
11
+ ## Key Features
12
+
13
+ * 🛡️ **WAF Fingerprinting:** Identifies 10+ major WAF vendors (Cloudflare, Akamai, AWS, Imperva, etc.) via signature-based and behavioral analysis.
14
+ * 🕵️ **Stealth-First Detection:** Implements adaptive jitter, randomized headers, and low-signal request patterns to bypass basic rate-limiters and heuristics.
15
+ * 🚦 **IDS/IPS Probing:** Uses low-level TCP signals and HTTP response anomalies to detect deep packet inspection and network-level interception.
16
+ * 🚫 **Fail2Ban Discovery:** Safely identifies SSH tarpits, "honey-pots," and active ban policies through non-destructive authentication probing.
17
+ * 🎨 **Modern Interface:** Built with `rich` for professional, structured terminal output and high-visibility results.
18
+ * 🤖 **Automation Ready:** Native JSON output mode for seamless integration into larger security pipelines.
19
+
20
+ ## Installation
21
+
22
+ ### The Modern Way (Recommended)
23
+ Use [uv](https://github.com/astral-sh/uv) for the fastest experience:
24
+
25
+ ```bash
26
+ # Run instantly without installing
27
+ uvx is-it-safe example.com
28
+
29
+ # Or install it
30
+ uv pip install is-it-safe
31
+ ```
32
+
33
+ ### The Traditional Way
34
+ ```bash
35
+ pip install is-it-safe
36
+ ```
37
+
38
+ ### From Source
39
+ ```bash
40
+ git clone https://github.com/your-username/is-it-safe.git
41
+ cd is-it-safe
42
+ pip install .
43
+ ```
44
+
45
+ ## 🛠 Usage
46
+
47
+ ```bash
48
+ # Basic scan
49
+ is-it-safe example.com
50
+
51
+ # Verbose scan with stealth enabled
52
+ is-it-safe example.com --stealth --verbose
53
+
54
+ # Scan specific SSH port for Fail2Ban
55
+ sudo is-it-safe example.com --ssh-port 2222
56
+
57
+ # Output results as JSON
58
+ is-it-safe example.com --json > results.json
59
+ ```
60
+
61
+ > [!IMPORTANT]
62
+ > Some IDS/IPS detection features require **root privileges** for raw socket access.
63
+
64
+ ## 📜 License
65
+
66
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
67
+
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "is-it-safe"
7
+ version = "5.0.0"
8
+ authors = [
9
+ { name="Mithun", email="mitchastertheblaster@gmail.com" },
10
+ ]
11
+ description = "Stealthy Security Layer Detector (WAF/IDS/IPS/Fail2Ban)"
12
+ readme = "README.md"
13
+ requires-python = ">=3.8"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Topic :: Security",
19
+ ]
20
+ dependencies = [
21
+ "requests>=2.31.0",
22
+ "urllib3>=2.0.0",
23
+ "rich>=13.7.0",
24
+ "ipaddress>=1.0.23",
25
+ "scapy>=2.5.0",
26
+ "paramiko>=3.4.0",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ test = [
31
+ "pytest>=7.4.0",
32
+ "pytest-mock>=3.12.0",
33
+ ]
34
+
35
+ [project.urls]
36
+ "Homepage" = "https://github.com/mithun/is-it-safe"
37
+ "Bug Tracker" = "https://github.com/mithun/is-it-safe/issues"
38
+
39
+ [project.scripts]
40
+ is-it-safe = "is_it_safe.main:main"
41
+
42
+ [tool.setuptools.packages.find]
43
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import sys
4
+ import logging
5
+ import json
6
+ from typing import Optional, Dict, Any, List
7
+ import urllib3
8
+ from urllib.parse import urlparse
9
+
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+ from rich.panel import Panel
13
+ from rich.logging import RichHandler
14
+ from rich.theme import Theme
15
+
16
+ from .modules.waf import detect_waf
17
+ from .modules.ids_ips import detect_ids_ips
18
+ from .modules.network import identify_network_layer
19
+ from .modules.fail2ban import detect_fail2ban
20
+
21
+ # Suppress insecure request warnings
22
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
23
+
24
+ # Custom theme for professional look
25
+ custom_theme = Theme({
26
+ "info": "cyan",
27
+ "warning": "yellow",
28
+ "error": "bold red",
29
+ "success": "bold green",
30
+ "high": "bold green",
31
+ "medium": "bold yellow",
32
+ "low": "dim white",
33
+ })
34
+
35
+ console = Console(theme=custom_theme)
36
+
37
+ def setup_logging(verbose: bool):
38
+ """Configure logging with Rich."""
39
+ logging.basicConfig(
40
+ level=logging.DEBUG if verbose else logging.WARNING,
41
+ format="%(message)s",
42
+ datefmt="[%X]",
43
+ handlers=[RichHandler(rich_tracebacks=True, console=console)]
44
+ )
45
+
46
+ # Suppress verbose urllib3 retry warnings
47
+ logging.getLogger('urllib3').setLevel(logging.ERROR)
48
+
49
+ def banner():
50
+ """Print a stylish banner."""
51
+ banner_text = """
52
+ ╔╦╗╔═╗ ╦╔╦╗ ╔═╗╔═╗╔═╗╔═╗
53
+ ║ ╚═╗ ║ ║ ╚═╗╠═╣╠╣ ║╣
54
+ ╩ ╚═╝ ╩ ╩ ╚═╝╩ ╩╚ ╚═╝
55
+ Stealthy Security Layer Detector v5.0
56
+ """
57
+ console.print(Panel(banner_text, style="info", expand=False))
58
+
59
+ def validate_url(target: str) -> tuple[Optional[str], Optional[str], Optional[str]]:
60
+ """Validate and normalize the target URL."""
61
+ if not target:
62
+ return None, None, "No target provided"
63
+
64
+ if not target.startswith(('http://', 'https://')):
65
+ normalized_url = f"https://{target}"
66
+ else:
67
+ normalized_url = target
68
+
69
+ try:
70
+ parsed = urlparse(normalized_url)
71
+ if not parsed.netloc:
72
+ return None, target, "Missing hostname"
73
+ return normalized_url, parsed.netloc, None
74
+ except Exception as e:
75
+ return None, None, f"URL parsing error: {e}"
76
+
77
+ def display_results(results: Dict[str, Any], verbose: bool):
78
+ """Display scan results in a professional table."""
79
+ table = Table(title=f"Scan Results for: [bold]{results['target']}[/bold]", show_header=True, header_style="bold magenta")
80
+ table.add_column("Category", style="cyan")
81
+ table.add_column("Detection", style="white")
82
+ table.add_column("Confidence", justify="center")
83
+
84
+ categories = {
85
+ "WAF": results["waf"],
86
+ "Network": results["network"],
87
+ "Fail2Ban": results["fail2ban"],
88
+ "IDS/IPS": results["ids_ips"]
89
+ }
90
+
91
+ for cat_name, detections in categories.items():
92
+ for i, d in enumerate(detections):
93
+ conf = d.get("confidence", "low")
94
+ conf_style = f"[{conf}]{conf}[/]"
95
+
96
+ row_name = cat_name if i == 0 else ""
97
+ table.add_row(row_name, d["name"], conf_style)
98
+
99
+ if verbose and d.get("details"):
100
+ table.add_row("", f"[dim]> {d['details']}[/dim]", "")
101
+
102
+ table.add_section()
103
+
104
+ console.print(table)
105
+
106
+ def main():
107
+ parser = argparse.ArgumentParser(
108
+ description="is-it-safe: Stealthy WAF/IDS/IPS/fail2ban detector v5.0",
109
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter
110
+ )
111
+ parser.add_argument("target", nargs="?", help="Target URL, Hostname, or IP")
112
+ parser.add_argument("-v", "--version", action="store_true", help="Show version and exit")
113
+ parser.add_argument("-s", "--stealth", action="store_true", help="Enable maximum stealth (higher jitter)")
114
+ parser.add_argument("--timeout", type=int, default=10, help="Request timeout in seconds")
115
+ parser.add_argument("--jitter", action="store_true", help="Add random delays between requests")
116
+ parser.add_argument("--verbose", action="store_true", help="Show detailed detection signals")
117
+ parser.add_argument("--json", action="store_true", help="Output results in JSON format")
118
+ parser.add_argument("--ssh-port", type=int, default=22, help="SSH port for fail2ban detection")
119
+ args = parser.parse_args()
120
+
121
+ if args.version:
122
+ console.print("is-it-safe v5.0.0")
123
+ sys.exit(0)
124
+
125
+ if not args.target:
126
+ parser.print_help()
127
+ console.print("\n[info]Example:[/] [bold]is-it-safe example.com[/bold]")
128
+ sys.exit(1)
129
+
130
+ setup_logging(args.verbose)
131
+
132
+ url, host, error = validate_url(args.target)
133
+
134
+ if not host:
135
+ console.print(f"[error]Error:[/] {error}")
136
+ sys.exit(1)
137
+
138
+ if not args.json:
139
+ banner()
140
+ console.print(f"[*] Starting scan for [bold cyan]{host}[/]...")
141
+
142
+ # Check if HTTP service is available
143
+ http_available = url is not None
144
+
145
+ with console.status("[bold green]Scanning security layers...") as status:
146
+ if http_available:
147
+ status.update("[bold green]Detecting WAF...")
148
+ waf_results = detect_waf(url, timeout=args.timeout, jitter=args.jitter or args.stealth)
149
+
150
+ status.update("[bold green]Identifying Network Layer...")
151
+ network_results = identify_network_layer(url, host=host)
152
+ else:
153
+ waf_results = [{"name": "No HTTP service detected", "confidence": "low", "details": "Target has no web service on port 80/443"}]
154
+ network_results = [{"name": "No HTTP service detected", "confidence": "low", "details": "Target has no web service on port 80/443"}]
155
+
156
+ status.update("[bold green]Probing for Fail2Ban (SSH)...")
157
+ fail2ban_results = detect_fail2ban(host, port=args.ssh_port)
158
+
159
+ status.update("[bold green]Testing IDS/IPS signals...")
160
+ if http_available:
161
+ ids_results = detect_ids_ips(host, url=url, use_tcp=True)
162
+ else:
163
+ ids_results = [{"name": "No HTTP service", "confidence": "low", "details": "Skipped - no web service to test"}]
164
+
165
+ results = {
166
+ "target": host,
167
+ "http_available": http_available,
168
+ "waf": waf_results,
169
+ "network": network_results,
170
+ "fail2ban": fail2ban_results,
171
+ "ids_ips": ids_results
172
+ }
173
+
174
+ if args.json:
175
+ print(json.dumps(results, indent=2))
176
+ else:
177
+ display_results(results, args.verbose)
178
+ console.print("\n[success]Scan complete.[/]")
179
+
180
+ if __name__ == "__main__":
181
+ main()
File without changes
@@ -0,0 +1,176 @@
1
+ """fail2ban detection module for is-it-safe."""
2
+ import logging
3
+ import socket
4
+ import time
5
+ import ipaddress
6
+ from typing import List, Dict, Tuple, Optional
7
+ from .utils import resolve_host
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ PARAMIKO_AVAILABLE = False
12
+ try:
13
+ import paramiko # type: ignore
14
+ PARAMIKO_AVAILABLE = True
15
+ except ImportError:
16
+ logger.warning("paramiko not installed - SSH detection limited. Install: pip install paramiko")
17
+
18
+ INVALID_USERNAMES = ["admin", "root", "user", "test", "guest", "oracle", "ubuntu"]
19
+ DEFAULT_SSH_PORT = 22
20
+ MAX_ATTEMPTS = 4
21
+
22
+ def is_valid_target(target: str) -> Tuple[bool, Optional[str]]:
23
+ """Validate that target is a valid IP or hostname."""
24
+ if not target:
25
+ return False, "No target provided"
26
+
27
+ try:
28
+ ipaddress.ip_address(target)
29
+ return True, None
30
+ except ValueError:
31
+ pass
32
+
33
+ if len(target) < 1 or len(target) > 253:
34
+ return False, "Invalid hostname length"
35
+
36
+ allowed = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-.")
37
+ if not all(c in allowed for c in target):
38
+ return False, "Invalid characters in target"
39
+
40
+ return True, None
41
+
42
+ def check_ssh_banner(target_host: str, port: int = 22, timeout: int = 5) -> Optional[str]:
43
+ """Grab SSH banner to identify service."""
44
+ try:
45
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
46
+ sock.settimeout(timeout)
47
+ sock.connect((target_host, port))
48
+ banner = sock.recv(1024).decode('utf-8', errors='ignore').strip()
49
+ return banner
50
+ except Exception as e:
51
+ logger.debug(f"SSH banner grab failed: {e}")
52
+ return None
53
+
54
+ def detect_fail2ban_ssh(target_host: str, port: int = 22, timeout: int = 5) -> List[Dict[str, str]]:
55
+ """Detect fail2ban by probing SSH with multiple auth attempts."""
56
+ results = []
57
+
58
+ if not PARAMIKO_AVAILABLE:
59
+ return [{"name": "paramiko not installed", "confidence": "low",
60
+ "details": "pip install paramiko for SSH detection"}]
61
+
62
+ try:
63
+ client = paramiko.SSHClient()
64
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
65
+
66
+ for i, username in enumerate(INVALID_USERNAMES[:MAX_ATTEMPTS]):
67
+ start_time = time.time()
68
+ try:
69
+ client.connect(
70
+ target_host,
71
+ port=port,
72
+ username=username,
73
+ password="wrong_password_12345",
74
+ timeout=timeout,
75
+ allow_agent=False,
76
+ look_for_keys=False
77
+ )
78
+ client.close()
79
+ except paramiko.AuthenticationException:
80
+ elapsed = time.time() - start_time
81
+ if elapsed > 3:
82
+ results.append({
83
+ "name": "Fail2Ban SSH",
84
+ "confidence": "high",
85
+ "details": f"Slow auth ({elapsed:.1f}s) after {i+1} attempts - possible tarpit"
86
+ })
87
+ break
88
+ except paramiko.SSHException as e:
89
+ error_msg = str(e).lower()
90
+ if "too many" in error_msg or "ban" in error_msg or "denied" in error_msg:
91
+ results.append({
92
+ "name": "Fail2Ban SSH",
93
+ "confidence": "high",
94
+ "details": f"Blocked: {e}"
95
+ })
96
+ break
97
+ except socket.timeout:
98
+ results.append({
99
+ "name": "Fail2Ban SSH",
100
+ "confidence": "medium",
101
+ "details": "Connection timeout - possible SSH tarpit"
102
+ })
103
+ break
104
+ except EOFError:
105
+ results.append({
106
+ "name": "Fail2Ban SSH",
107
+ "confidence": "high",
108
+ "details": "EOF received - possibly banned"
109
+ })
110
+ break
111
+ except Exception as e:
112
+ logger.debug(f"SSH probe {i+1} error: {e}")
113
+
114
+ time.sleep(0.5)
115
+
116
+ except Exception as e:
117
+ logger.debug(f"SSH Detection error: {e}")
118
+
119
+ return results
120
+
121
+ def detect_ssh_service(target_host: str, port: int = 22) -> List[Dict[str, str]]:
122
+ """Basic SSH service detection."""
123
+ results = []
124
+
125
+ banner = check_ssh_banner(target_host, port)
126
+ if banner:
127
+ if "openssh" in banner.lower():
128
+ results.append({
129
+ "name": "SSH (OpenSSH)",
130
+ "confidence": "high",
131
+ "details": banner.strip()
132
+ })
133
+ elif "dropbear" in banner.lower():
134
+ results.append({
135
+ "name": "SSH (Dropbear)",
136
+ "confidence": "high",
137
+ "details": banner.strip()
138
+ })
139
+ else:
140
+ results.append({
141
+ "name": "SSH Service",
142
+ "confidence": "medium",
143
+ "details": banner.strip()
144
+ })
145
+
146
+ return results
147
+
148
+ def detect_fail2ban(target: str, port: int = 22) -> List[Dict[str, str]]:
149
+ """Combined fail2ban detection for SSH."""
150
+ all_results = []
151
+
152
+ valid, error = is_valid_target(target)
153
+ if not valid:
154
+ return [{"name": "Invalid target", "confidence": "low", "details": error or "Target validation failed"}]
155
+
156
+ host = target
157
+ if "://" in host:
158
+ host = host.split("://")[1].split("/")[0]
159
+
160
+ if not host:
161
+ return [{"name": "No target", "confidence": "low", "details": "Invalid target"}]
162
+
163
+ ssh_service = detect_ssh_service(host, port)
164
+ if not ssh_service:
165
+ all_results.append({"name": "No SSH service", "confidence": "low", "details": "SSH port closed/not found"})
166
+ return all_results
167
+
168
+ all_results.extend(ssh_service)
169
+
170
+ if PARAMIKO_AVAILABLE:
171
+ fail2ban_results = detect_fail2ban_ssh(host, port)
172
+ all_results.extend(fail2ban_results)
173
+ else:
174
+ all_results.append({"name": "paramiko needed", "confidence": "low", "details": "Install paramiko for fail2ban detection"})
175
+
176
+ return all_results
@@ -0,0 +1,104 @@
1
+ """IDS/IPS detection module for is-it-safe."""
2
+ import os
3
+ import logging
4
+ import time
5
+ from typing import List, Dict, Any, Optional
6
+ from .utils import safe_request
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ SCAPY_AVAILABLE = False
11
+ try:
12
+ from scapy.all import IP, TCP, sr1 # type: ignore
13
+ SCAPY_AVAILABLE = True
14
+ except ImportError:
15
+ logger.warning("scapy not installed - TCP-level IDS/IPS detection disabled")
16
+
17
+ def detect_ids_ips_tcp(target_host: str, port: int = 80) -> List[Dict[str, str]]:
18
+ """Detect IDS/IPS using TCP-level signals."""
19
+ results = []
20
+
21
+ if not SCAPY_AVAILABLE:
22
+ results.append({"name": "scapy required", "confidence": "low", "details": "Install scapy for TCP detection: pip install scapy"})
23
+ return results
24
+
25
+ if os.geteuid() != 0:
26
+ results.append({"name": "root required", "confidence": "low", "details": "Run as root for TCP-level IDS/IPS detection"})
27
+ return results
28
+
29
+ try:
30
+ # 1. Test for TCP Reset on suspicious payload-like sequence numbers or unusual flags
31
+ pkt_syn = IP(dst=target_host)/TCP(dport=port, flags="S", seq=12345)
32
+ start = time.time()
33
+ ans = sr1(pkt_syn, timeout=2, verbose=0)
34
+
35
+ if not ans:
36
+ results.append({"name": "Packet Drop", "confidence": "medium", "details": "No response to SYN packet"})
37
+ elif ans.haslayer(TCP):
38
+ if ans[TCP].flags == 0x14: # RST-ACK
39
+ results.append({"name": "TCP Reset", "confidence": "high", "details": "Immediate RST received"})
40
+
41
+ # 2. Timing anomalies (Latency spikes)
42
+ latencies = []
43
+ for _ in range(3):
44
+ s = time.time()
45
+ sr1(IP(dst=target_host)/TCP(dport=port, flags="S"), timeout=1, verbose=0)
46
+ latencies.append(time.time() - s)
47
+
48
+ if max(latencies) - min(latencies) > 0.5:
49
+ results.append({"name": "Timing Anomaly", "confidence": "low", "details": "Jitter/latency spike detected"})
50
+
51
+ except Exception as e:
52
+ logger.warning(f"TCP IDS/IPS detection error: {e}")
53
+
54
+ return results
55
+
56
+ def detect_ids_ips_http(target_url: str) -> List[Dict[str, str]]:
57
+ """Detect IDS/IPS using HTTP-level signals (Rate limiting, blocking)."""
58
+ results = []
59
+
60
+ # 1. Rate limiting behavior
61
+ codes = []
62
+ for _ in range(5):
63
+ resp = safe_request(target_url, timeout=5)
64
+ if resp:
65
+ codes.append(resp.status_code)
66
+ else:
67
+ codes.append(None)
68
+
69
+ if 429 in codes:
70
+ results.append({"name": "Rate Limiting", "confidence": "high", "details": "HTTP 429 received after rapid requests"})
71
+ elif codes.count(None) >= 2:
72
+ results.append({"name": "Connection Instability", "confidence": "medium", "details": "Intermittent connection drops detected"})
73
+
74
+ # 2. Suspicious payload blocking
75
+ suspicious_payloads = ["/etc/passwd", "?id=1' OR '1'='1"]
76
+ for p in suspicious_payloads:
77
+ try:
78
+ resp = safe_request(target_url + p, timeout=5)
79
+ if resp is None:
80
+ results.append({"name": "Likely IPS", "confidence": "high", "details": f"Connection reset on payload: {p}"})
81
+ except Exception:
82
+ pass
83
+
84
+ return results
85
+
86
+ def detect_ids_ips(target: str, url: Optional[str] = None, use_tcp: bool = True) -> List[Dict[str, str]]:
87
+ """Combined IDS/IPS detection."""
88
+ all_results = []
89
+
90
+ if url:
91
+ all_results.extend(detect_ids_ips_http(url))
92
+
93
+ if use_tcp:
94
+ host = target
95
+ if "://" in host:
96
+ host = host.split("://")[1].split("/")[0]
97
+
98
+ tcp_res = detect_ids_ips_tcp(host)
99
+ all_results.extend(tcp_res)
100
+
101
+ if not all_results:
102
+ return [{"name": "No strong evidence", "confidence": "low", "details": "No IDS/IPS signals detected"}]
103
+
104
+ return all_results
@@ -0,0 +1,53 @@
1
+ """Network layer identification module for is-it-safe."""
2
+ import logging
3
+ import socket
4
+ from typing import List, Dict
5
+ from .utils import safe_request, resolve_host
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ CDN_SIGNATURES = {
10
+ "Cloudflare": ["cloudflare", "cf-ray"],
11
+ "Akamai": ["akamai", "edgekey"],
12
+ "AWS CloudFront": ["cloudfront"],
13
+ "Fastly": ["fastly"],
14
+ "Google Cloud": ["google"],
15
+ "Azure Front Door": ["azure"],
16
+ "Incapsula": ["incapsula", "imperva"]
17
+ }
18
+
19
+ def identify_network_layer(url: str, host: str) -> List[Dict[str, str]]:
20
+ """Identify CDN and infrastructure providers."""
21
+ results = []
22
+
23
+ # 1. DNS-based identification (CNAME/PTR)
24
+ try:
25
+ # Basic reverse DNS check if possible, or just look for known patterns in hostname
26
+ addr = resolve_host(host)
27
+ if addr:
28
+ try:
29
+ ptr = socket.gethostbyaddr(addr)[0]
30
+ for provider, sigs in CDN_SIGNATURES.items():
31
+ for sig in sigs:
32
+ if sig in ptr.lower():
33
+ results.append({"name": f"{provider} (DNS)", "confidence": "high", "details": f"PTR: {ptr}"})
34
+ break
35
+ except (socket.herror, socket.gaierror):
36
+ pass
37
+ except Exception as e:
38
+ logger.debug(f"DNS identification error: {e}")
39
+
40
+ # 2. Header-based identification (redundant with WAF but good for network layer)
41
+ resp = safe_request(url, timeout=5)
42
+ if resp:
43
+ server_header = resp.headers.get("Server", "").lower()
44
+ for provider, sigs in CDN_SIGNATURES.items():
45
+ for sig in sigs:
46
+ if sig in server_header:
47
+ results.append({"name": f"{provider} (Header)", "confidence": "high", "details": f"Server: {resp.headers.get('Server')}"})
48
+ break
49
+
50
+ if not results:
51
+ results.append({"name": "Generic Infrastructure", "confidence": "low", "details": "No specific CDN/Cloud provider detected"})
52
+
53
+ return results
@@ -0,0 +1,101 @@
1
+ """Utility functions for is-it-safe."""
2
+ import logging
3
+ import random
4
+ import time
5
+ import socket
6
+ from typing import Optional, Dict, List, Any
7
+ import requests
8
+ from requests.adapters import HTTPAdapter
9
+ from urllib3.util.retry import Retry
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ USER_AGENTS = [
14
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
15
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
16
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
17
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
18
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
19
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/120.0.0.0"
20
+ ]
21
+
22
+ def get_random_headers() -> Dict[str, str]:
23
+ """Return randomized HTTP headers."""
24
+ return {
25
+ "User-Agent": random.choice(USER_AGENTS),
26
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
27
+ "Accept-Language": "en-US,en;q=0.5",
28
+ "Accept-Encoding": "gzip, deflate, br",
29
+ "Connection": "keep-alive",
30
+ "Upgrade-Insecure-Requests": "1",
31
+ "Sec-Fetch-Dest": "document",
32
+ "Sec-Fetch-Mode": "navigate",
33
+ "Sec-Fetch-Site": "none",
34
+ "Sec-Fetch-User": "?1",
35
+ "DNT": "1"
36
+ }
37
+
38
+ def apply_jitter(enabled: bool = True, min_delay: float = 0.5, max_delay: float = 1.5) -> None:
39
+ """Add random delay between requests for stealth."""
40
+ if enabled:
41
+ delay = random.uniform(min_delay, max_delay)
42
+ logger.debug(f"Applying jitter: {delay:.2f}s")
43
+ time.sleep(delay)
44
+
45
+ def resolve_host(target: str) -> Optional[str]:
46
+ """Resolve hostname to IP address."""
47
+ try:
48
+ # Strip protocol if present
49
+ if "://" in target:
50
+ target = target.split("://")[1].split("/")[0]
51
+ return socket.gethostbyname(target)
52
+ except socket.gaierror:
53
+ return None
54
+
55
+ def safe_request(url: str, timeout: int = 10, allow_redirects: bool = True, headers: Optional[Dict[str, str]] = None) -> Optional[requests.Response]:
56
+ """Make a safe HTTP request with retries and error handling."""
57
+ if headers is None:
58
+ headers = get_random_headers()
59
+
60
+ session = requests.Session()
61
+ retries = Retry(total=2, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504])
62
+ session.mount('http://', HTTPAdapter(max_retries=retries))
63
+ session.mount('https://', HTTPAdapter(max_retries=retries))
64
+
65
+ try:
66
+ response = session.get(
67
+ url,
68
+ headers=headers,
69
+ timeout=timeout,
70
+ allow_redirects=allow_redirects,
71
+ verify=True
72
+ )
73
+ return response
74
+ except requests.exceptions.SSLError as e:
75
+ logger.warning(f"SSL verification failed for {url}, falling back to verify=False: {e}")
76
+ try:
77
+ response = session.get(
78
+ url,
79
+ headers=headers,
80
+ timeout=timeout,
81
+ allow_redirects=allow_redirects,
82
+ verify=False
83
+ )
84
+ return response
85
+ except requests.RequestException as e:
86
+ logger.debug(f"Request error for {url}: {e}")
87
+ return None
88
+ except requests.RequestException as e:
89
+ logger.debug(f"Request error for {url}: {e}")
90
+ return None
91
+
92
+ def calculate_confidence(matches: int, total_signals: int) -> str:
93
+ """Calculate confidence level based on signal matches."""
94
+ if total_signals == 0:
95
+ return "low"
96
+ ratio = matches / total_signals
97
+ if ratio >= 0.8:
98
+ return "high"
99
+ elif ratio >= 0.4:
100
+ return "medium"
101
+ return "low"
@@ -0,0 +1,149 @@
1
+ """WAF detection module for is-it-safe."""
2
+ import logging
3
+ from typing import List, Dict, Any, Optional, Set
4
+ from .utils import safe_request, apply_jitter
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ WAF_SIGNATURES = {
9
+ "Cloudflare": {
10
+ "headers": ["cf-ray", "cf-cache-status", "server: cloudflare"],
11
+ "cookies": ["__cfduid", "cf_clearance"],
12
+ "confidence": "high"
13
+ },
14
+ "Akamai": {
15
+ "headers": ["x-akamai", "akamai-ghost", "server: akamaighost"],
16
+ "cookies": ["ak_bmsc", "bm_sz"],
17
+ "confidence": "high"
18
+ },
19
+ "AWS CloudFront": {
20
+ "headers": ["x-amz-cf-id", "x-amz-cf-pop", "server: cloudfront"],
21
+ "cookies": [],
22
+ "confidence": "high"
23
+ },
24
+ "Imperva/Incapsula": {
25
+ "headers": ["x-iinfo", "incap_ses", "x-cdn: incapsula"],
26
+ "cookies": ["incap_ses", "visid_incap"],
27
+ "confidence": "high"
28
+ },
29
+ "Sucuri": {
30
+ "headers": ["x-sucuri-id", "x-sucuri-cache", "server: sucuri/cloudproxy"],
31
+ "cookies": ["sucuri_cloudproxy_uuid"],
32
+ "confidence": "high"
33
+ },
34
+ "F5 BIG-IP": {
35
+ "headers": ["x-cpro-rule", "server: big-ip", "x-wa-info"],
36
+ "cookies": ["bigipserver", "mrhsessions"],
37
+ "confidence": "medium"
38
+ },
39
+ "ModSecurity": {
40
+ "headers": ["x-mod-security", "server: mod_security"],
41
+ "cookies": ["noYB"],
42
+ "confidence": "medium"
43
+ },
44
+ "Barracuda": {
45
+ "headers": ["server: barracuda", "x-barracuda-brts"],
46
+ "cookies": ["barra_counter_session", "bni__b_pool"],
47
+ "confidence": "medium"
48
+ },
49
+ "FortiWeb": {
50
+ "headers": ["server: fortiweb-waf"],
51
+ "cookies": ["fortiwafsid"],
52
+ "confidence": "high"
53
+ },
54
+ "Radware AppWall": {
55
+ "headers": ["x-sl-compid", "server: radware"],
56
+ "cookies": [],
57
+ "confidence": "medium"
58
+ }
59
+ }
60
+
61
+ BENIGN_PAYLOADS = ["", "?id=1", "/favicon.ico"]
62
+ SUSPICIOUS_PAYLOADS = ["?id=1' OR '1'='1", "?id=<script>alert(1)</script>", "/etc/passwd", "?cmd=ls"]
63
+
64
+ def check_response_for_waf(response: Any, waf_name: str) -> bool:
65
+ """Check if response headers/cookies match WAF signatures."""
66
+ if not response:
67
+ return False
68
+
69
+ sigs = WAF_SIGNATURES[waf_name]
70
+ headers_str = "\n".join([f"{k}: {v}" for k, v in response.headers.items()]).lower()
71
+
72
+ for header in sigs["headers"]:
73
+ if header.lower() in headers_str:
74
+ return True
75
+
76
+ for cookie in response.cookies.keys():
77
+ for sig_cookie in sigs["cookies"]:
78
+ if sig_cookie.lower() in cookie.lower():
79
+ return True
80
+
81
+ return False
82
+
83
+ def test_response_behavior(url: str, jitter: bool = True) -> Optional[Dict[str, Any]]:
84
+ """Compare behavior between benign and suspicious payloads."""
85
+ apply_jitter(enabled=jitter)
86
+ benign_resp = safe_request(url)
87
+ if not benign_resp:
88
+ return None
89
+
90
+ apply_jitter(enabled=jitter)
91
+ for payload in SUSPICIOUS_PAYLOADS:
92
+ # Properly append query params
93
+ if "?" in url:
94
+ suspicious_url = url + "&" + payload.lstrip("?")
95
+ else:
96
+ suspicious_url = url + payload
97
+ resp = safe_request(suspicious_url)
98
+
99
+ if not resp:
100
+ # Connection dropped/reset often indicates a WAF/IPS
101
+ return {"type": "Connection Drop", "payload": payload}
102
+
103
+ if resp.status_code != benign_resp.status_code:
104
+ if resp.status_code in [403, 406, 501, 429, 999]:
105
+ return {"type": "Status Code Change", "code": resp.status_code, "payload": payload}
106
+
107
+ # Check for WAF strings in body (some WAFs return 200 with a block page)
108
+ block_keywords = ["blocked by", "waf", "security challenge", "incident id", "request id"]
109
+ for kw in block_keywords:
110
+ if kw in resp.text.lower() and kw not in benign_resp.text.lower():
111
+ return {"type": "Block Page Content", "keyword": kw, "payload": payload}
112
+
113
+ return None
114
+
115
+ def detect_waf(target_url: str, timeout: int = 10, jitter: bool = True) -> List[Dict[str, str]]:
116
+ """Detect WAF with confidence scoring."""
117
+ results = []
118
+ matched_wafs: Set[str] = set()
119
+
120
+ response = safe_request(target_url, timeout=timeout)
121
+ if not response:
122
+ return [{"name": "Unable to connect", "confidence": "low", "details": "Initial request failed"}]
123
+
124
+ # Signature matching - only one WAF per detection
125
+ for waf_name in WAF_SIGNATURES:
126
+ if check_response_for_waf(response, waf_name):
127
+ matched_wafs.add(waf_name)
128
+
129
+ # Prioritize high confidence matches
130
+ for waf_name in matched_wafs:
131
+ results.append({
132
+ "name": waf_name,
133
+ "confidence": WAF_SIGNATURES[waf_name]["confidence"],
134
+ "details": "Signature match in headers/cookies"
135
+ })
136
+
137
+ # Behavioral testing
138
+ behavior = test_response_behavior(target_url, jitter)
139
+ if behavior:
140
+ results.append({
141
+ "name": "Generic Behavioral WAF",
142
+ "confidence": "medium",
143
+ "details": f"Behavioral anomaly: {behavior['type']} on payload '{behavior.get('payload', '')}'"
144
+ })
145
+
146
+ if not results:
147
+ results.append({"name": "No WAF detected", "confidence": "low", "details": "No signatures or behavioral anomalies found"})
148
+
149
+ return results
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: is-it-safe
3
+ Version: 5.0.0
4
+ Summary: Stealthy Security Layer Detector (WAF/IDS/IPS/Fail2Ban)
5
+ Author-email: Mithun <mitchastertheblaster@gmail.com>
6
+ Project-URL: Homepage, https://github.com/mithun/is-it-safe
7
+ Project-URL: Bug Tracker, https://github.com/mithun/is-it-safe/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Topic :: Security
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: requests>=2.31.0
16
+ Requires-Dist: urllib3>=2.0.0
17
+ Requires-Dist: rich>=13.7.0
18
+ Requires-Dist: ipaddress>=1.0.23
19
+ Requires-Dist: scapy>=2.5.0
20
+ Requires-Dist: paramiko>=3.4.0
21
+ Provides-Extra: test
22
+ Requires-Dist: pytest>=7.4.0; extra == "test"
23
+ Requires-Dist: pytest-mock>=3.12.0; extra == "test"
24
+ Dynamic: license-file
25
+
26
+ # is-it-safe 🛡️
27
+
28
+ [![PyPI version](https://img.shields.io/pypi/v/is-it-safe.svg)](https://pypi.org/project/is-it-safe/)
29
+ [![Python versions](https://img.shields.io/pypi/pyversions/is-it-safe.svg)](https://pypi.org/project/is-it-safe/)
30
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
31
+
32
+ **Stealthy Security Layer Fingerprinting & Detection v5.0**
33
+
34
+ `is-it-safe` is a modern, high-performance security utility designed to map and identify protective layers surrounding a target without triggering aggressive defense mechanisms. It provides deep visibility into infrastructure security by fingerprinting WAFs, IDS/IPS, and automated blocking systems.
35
+
36
+ ## Key Features
37
+
38
+ * 🛡️ **WAF Fingerprinting:** Identifies 10+ major WAF vendors (Cloudflare, Akamai, AWS, Imperva, etc.) via signature-based and behavioral analysis.
39
+ * 🕵️ **Stealth-First Detection:** Implements adaptive jitter, randomized headers, and low-signal request patterns to bypass basic rate-limiters and heuristics.
40
+ * 🚦 **IDS/IPS Probing:** Uses low-level TCP signals and HTTP response anomalies to detect deep packet inspection and network-level interception.
41
+ * 🚫 **Fail2Ban Discovery:** Safely identifies SSH tarpits, "honey-pots," and active ban policies through non-destructive authentication probing.
42
+ * 🎨 **Modern Interface:** Built with `rich` for professional, structured terminal output and high-visibility results.
43
+ * 🤖 **Automation Ready:** Native JSON output mode for seamless integration into larger security pipelines.
44
+
45
+ ## Installation
46
+
47
+ ### The Modern Way (Recommended)
48
+ Use [uv](https://github.com/astral-sh/uv) for the fastest experience:
49
+
50
+ ```bash
51
+ # Run instantly without installing
52
+ uvx is-it-safe example.com
53
+
54
+ # Or install it
55
+ uv pip install is-it-safe
56
+ ```
57
+
58
+ ### The Traditional Way
59
+ ```bash
60
+ pip install is-it-safe
61
+ ```
62
+
63
+ ### From Source
64
+ ```bash
65
+ git clone https://github.com/your-username/is-it-safe.git
66
+ cd is-it-safe
67
+ pip install .
68
+ ```
69
+
70
+ ## 🛠 Usage
71
+
72
+ ```bash
73
+ # Basic scan
74
+ is-it-safe example.com
75
+
76
+ # Verbose scan with stealth enabled
77
+ is-it-safe example.com --stealth --verbose
78
+
79
+ # Scan specific SSH port for Fail2Ban
80
+ sudo is-it-safe example.com --ssh-port 2222
81
+
82
+ # Output results as JSON
83
+ is-it-safe example.com --json > results.json
84
+ ```
85
+
86
+ > [!IMPORTANT]
87
+ > Some IDS/IPS detection features require **root privileges** for raw socket access.
88
+
89
+ ## 📜 License
90
+
91
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
92
+
@@ -0,0 +1,19 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/is_it_safe/__init__.py
5
+ src/is_it_safe/main.py
6
+ src/is_it_safe.egg-info/PKG-INFO
7
+ src/is_it_safe.egg-info/SOURCES.txt
8
+ src/is_it_safe.egg-info/dependency_links.txt
9
+ src/is_it_safe.egg-info/entry_points.txt
10
+ src/is_it_safe.egg-info/requires.txt
11
+ src/is_it_safe.egg-info/top_level.txt
12
+ src/is_it_safe/modules/__init__.py
13
+ src/is_it_safe/modules/fail2ban.py
14
+ src/is_it_safe/modules/ids_ips.py
15
+ src/is_it_safe/modules/network.py
16
+ src/is_it_safe/modules/utils.py
17
+ src/is_it_safe/modules/waf.py
18
+ tests/test_core.py
19
+ tests/test_waf.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ is-it-safe = is_it_safe.main:main
@@ -0,0 +1,10 @@
1
+ requests>=2.31.0
2
+ urllib3>=2.0.0
3
+ rich>=13.7.0
4
+ ipaddress>=1.0.23
5
+ scapy>=2.5.0
6
+ paramiko>=3.4.0
7
+
8
+ [test]
9
+ pytest>=7.4.0
10
+ pytest-mock>=3.12.0
@@ -0,0 +1 @@
1
+ is_it_safe
@@ -0,0 +1,31 @@
1
+ import pytest
2
+ from is_it_safe.main import validate_url
3
+ from is_it_safe.modules.utils import calculate_confidence
4
+
5
+ def test_validate_url():
6
+ # Test with protocol
7
+ url, host, error = validate_url("https://example.com")
8
+ assert url == "https://example.com"
9
+ assert host == "example.com"
10
+ assert error is None
11
+
12
+ # Test without protocol
13
+ url, host, error = validate_url("example.com")
14
+ assert url == "https://example.com"
15
+ assert host == "example.com"
16
+ assert error is None
17
+
18
+ # Test invalid URL
19
+ url, host, error = validate_url("")
20
+ assert url is None
21
+ assert host is None
22
+ assert error == "No target provided"
23
+
24
+ def test_calculate_confidence():
25
+ assert calculate_confidence(10, 10) == "high"
26
+ assert calculate_confidence(8, 10) == "high"
27
+ assert calculate_confidence(5, 10) == "medium"
28
+ assert calculate_confidence(4, 10) == "medium"
29
+ assert calculate_confidence(2, 10) == "low"
30
+ assert calculate_confidence(0, 10) == "low"
31
+ assert calculate_confidence(0, 0) == "low"
@@ -0,0 +1,40 @@
1
+ import pytest
2
+ from unittest.mock import MagicMock
3
+ from is_it_safe.modules.waf import check_response_for_waf, detect_waf
4
+
5
+ def test_check_response_for_waf():
6
+ # Mock response with Cloudflare headers
7
+ mock_resp = MagicMock()
8
+ mock_resp.headers = {"cf-ray": "12345", "server": "cloudflare"}
9
+ mock_resp.cookies = {}
10
+
11
+ assert check_response_for_waf(mock_resp, "Cloudflare") is True
12
+ assert check_response_for_waf(mock_resp, "Akamai") is False
13
+
14
+ def test_check_response_for_waf_cookies():
15
+ # Mock response with Akamai cookies
16
+ mock_resp = MagicMock()
17
+ mock_resp.headers = {}
18
+ mock_resp.cookies = {"ak_bmsc": "somevalue"}
19
+
20
+ assert check_response_for_waf(mock_resp, "Akamai") is True
21
+
22
+ @pytest.fixture
23
+ def mock_safe_request(mocker):
24
+ return mocker.patch("is_it_safe.modules.waf.safe_request")
25
+
26
+ def test_detect_waf_signature_match(mock_safe_request):
27
+ mock_resp = MagicMock()
28
+ mock_resp.headers = {"cf-ray": "12345"}
29
+ mock_resp.cookies = {}
30
+ mock_safe_request.return_value = mock_resp
31
+
32
+ results = detect_waf("https://example.com")
33
+ assert any(r["name"] == "Cloudflare" for r in results)
34
+ assert any(r["confidence"] == "high" for r in results)
35
+
36
+ def test_detect_waf_no_connection(mock_safe_request):
37
+ mock_safe_request.return_value = None
38
+
39
+ results = detect_waf("https://example.com")
40
+ assert results[0]["name"] == "Unable to connect"