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.
- is_it_safe-5.0.0/LICENSE +21 -0
- is_it_safe-5.0.0/PKG-INFO +92 -0
- is_it_safe-5.0.0/README.md +67 -0
- is_it_safe-5.0.0/pyproject.toml +43 -0
- is_it_safe-5.0.0/setup.cfg +4 -0
- is_it_safe-5.0.0/src/is_it_safe/__init__.py +0 -0
- is_it_safe-5.0.0/src/is_it_safe/main.py +181 -0
- is_it_safe-5.0.0/src/is_it_safe/modules/__init__.py +0 -0
- is_it_safe-5.0.0/src/is_it_safe/modules/fail2ban.py +176 -0
- is_it_safe-5.0.0/src/is_it_safe/modules/ids_ips.py +104 -0
- is_it_safe-5.0.0/src/is_it_safe/modules/network.py +53 -0
- is_it_safe-5.0.0/src/is_it_safe/modules/utils.py +101 -0
- is_it_safe-5.0.0/src/is_it_safe/modules/waf.py +149 -0
- is_it_safe-5.0.0/src/is_it_safe.egg-info/PKG-INFO +92 -0
- is_it_safe-5.0.0/src/is_it_safe.egg-info/SOURCES.txt +19 -0
- is_it_safe-5.0.0/src/is_it_safe.egg-info/dependency_links.txt +1 -0
- is_it_safe-5.0.0/src/is_it_safe.egg-info/entry_points.txt +2 -0
- is_it_safe-5.0.0/src/is_it_safe.egg-info/requires.txt +10 -0
- is_it_safe-5.0.0/src/is_it_safe.egg-info/top_level.txt +1 -0
- is_it_safe-5.0.0/tests/test_core.py +31 -0
- is_it_safe-5.0.0/tests/test_waf.py +40 -0
is_it_safe-5.0.0/LICENSE
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/is-it-safe/)
|
|
29
|
+
[](https://pypi.org/project/is-it-safe/)
|
|
30
|
+
[](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
|
+
[](https://pypi.org/project/is-it-safe/)
|
|
4
|
+
[](https://pypi.org/project/is-it-safe/)
|
|
5
|
+
[](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"]
|
|
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
|
+
[](https://pypi.org/project/is-it-safe/)
|
|
29
|
+
[](https://pypi.org/project/is-it-safe/)
|
|
30
|
+
[](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 @@
|
|
|
1
|
+
|
|
@@ -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"
|