nextog-cli 1.0.0__py3-none-any.whl
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.
- nextog/__init__.py +4 -0
- nextog/cli.py +545 -0
- nextog/config/__init__.py +1 -0
- nextog/config/settings.py +132 -0
- nextog/core/__init__.py +1 -0
- nextog/core/engine.py +193 -0
- nextog/core/permissions.py +129 -0
- nextog/core/privacy.py +130 -0
- nextog/core/reporter.py +204 -0
- nextog/core/runner.py +236 -0
- nextog/data/__init__.py +1 -0
- nextog/data/local_db.py +367 -0
- nextog/data/models.py +72 -0
- nextog/data/sync.py +65 -0
- nextog/engines/__init__.py +1 -0
- nextog/engines/api/__init__.py +1 -0
- nextog/engines/api/graphql.py +54 -0
- nextog/engines/api/rest.py +346 -0
- nextog/engines/api/websocket.py +59 -0
- nextog/engines/embedded/__init__.py +1 -0
- nextog/engines/embedded/firmware.py +53 -0
- nextog/engines/embedded/hardware.py +330 -0
- nextog/engines/mobile/__init__.py +1 -0
- nextog/engines/mobile/android.py +333 -0
- nextog/engines/mobile/cross.py +48 -0
- nextog/engines/mobile/ios.py +46 -0
- nextog/engines/system/__init__.py +1 -0
- nextog/engines/system/load.py +121 -0
- nextog/engines/system/performance.py +128 -0
- nextog/engines/system/security.py +170 -0
- nextog/engines/web/__init__.py +1 -0
- nextog/engines/web/accessibility.py +191 -0
- nextog/engines/web/browser.py +387 -0
- nextog/engines/web/elements.py +285 -0
- nextog/engines/web/responsive.py +79 -0
- nextog/live/__init__.py +1 -0
- nextog/live/dashboard.py +30 -0
- nextog/live/panel.py +325 -0
- nextog/reports/__init__.py +1359 -0
- nextog/training/__init__.py +1 -0
- nextog/training/learner.py +269 -0
- nextog/training/patterns.py +102 -0
- nextog/utils/__init__.py +1 -0
- nextog/utils/helpers.py +91 -0
- nextog/utils/logger.py +37 -0
- nextog/utils/validators.py +98 -0
- nextog_cli-1.0.0.dist-info/METADATA +344 -0
- nextog_cli-1.0.0.dist-info/RECORD +51 -0
- nextog_cli-1.0.0.dist-info/WHEEL +5 -0
- nextog_cli-1.0.0.dist-info/entry_points.txt +2 -0
- nextog_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Cross-platform mobile testing - runs tests on both Android and iOS"""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
console = Console()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CrossPlatformTestEngine:
|
|
10
|
+
"""Run tests across both Android and iOS platforms"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, settings, db, privacy):
|
|
13
|
+
self.settings = settings
|
|
14
|
+
self.db = db
|
|
15
|
+
self.privacy = privacy
|
|
16
|
+
|
|
17
|
+
def run_tests(self, android_app: str = None, ios_app: str = None,
|
|
18
|
+
test_suite: str = "default", coverage_target: int = 90) -> Dict:
|
|
19
|
+
"""Run same test suite on both platforms"""
|
|
20
|
+
results = {
|
|
21
|
+
"total": 0, "passed": 0, "failed": 0, "skipped": 0,
|
|
22
|
+
"coverage": 0, "platforms": {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Test on Android
|
|
26
|
+
if android_app:
|
|
27
|
+
from nextog.engines.mobile.android import MobileTestEngine
|
|
28
|
+
android_engine = MobileTestEngine(self.settings, self.db, self.privacy)
|
|
29
|
+
android_results = android_engine.run_tests(android_app, "android")
|
|
30
|
+
results["platforms"]["android"] = android_results
|
|
31
|
+
|
|
32
|
+
# Test on iOS
|
|
33
|
+
if ios_app:
|
|
34
|
+
from nextog.engines.mobile.ios import iOSTestEngine
|
|
35
|
+
ios_engine = iOSTestEngine(self.settings, self.db, self.privacy)
|
|
36
|
+
ios_results = ios_engine.run_tests(ios_app, "ios")
|
|
37
|
+
results["platforms"]["ios"] = ios_results
|
|
38
|
+
|
|
39
|
+
# Aggregate results
|
|
40
|
+
for platform_data in results["platforms"].values():
|
|
41
|
+
results["total"] += platform_data.get("total", 0)
|
|
42
|
+
results["passed"] += platform_data.get("passed", 0)
|
|
43
|
+
results["failed"] += platform_data.get("failed", 0)
|
|
44
|
+
|
|
45
|
+
if results["total"] > 0:
|
|
46
|
+
results["coverage"] = round((results["passed"] / results["total"]) * 100, 2)
|
|
47
|
+
|
|
48
|
+
return results
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""iOS Testing Module"""
|
|
2
|
+
|
|
3
|
+
from typing import Dict
|
|
4
|
+
from nextog.engines.mobile.android import MobileTestEngine
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class iOSTestEngine(MobileTestEngine):
|
|
8
|
+
"""iOS-specific testing via Appium XCUITest"""
|
|
9
|
+
|
|
10
|
+
def _setup_driver(self, app_path: str, platform: str, device: str):
|
|
11
|
+
"""Setup iOS Appium driver"""
|
|
12
|
+
try:
|
|
13
|
+
from appium import webdriver
|
|
14
|
+
from appium.options.ios import XCUITestOptions
|
|
15
|
+
|
|
16
|
+
options = XCUITestOptions()
|
|
17
|
+
options.platform_name = "iOS"
|
|
18
|
+
options.app = app_path
|
|
19
|
+
options.device_name = device or "iPhone 14"
|
|
20
|
+
options.automation_name = "XCUITest"
|
|
21
|
+
options.platform_version = "16.0"
|
|
22
|
+
options.no_reset = True
|
|
23
|
+
|
|
24
|
+
self.driver = webdriver.Remote("http://127.0.0.1:4723", options=options)
|
|
25
|
+
except ImportError:
|
|
26
|
+
raise ImportError("Install Appium-Python-Client for iOS testing")
|
|
27
|
+
|
|
28
|
+
def _test_ui_elements(self) -> Dict:
|
|
29
|
+
"""iOS-specific UI element tests"""
|
|
30
|
+
results = {"total": 0, "passed": 0, "failed": 0}
|
|
31
|
+
|
|
32
|
+
if not self.driver:
|
|
33
|
+
return results
|
|
34
|
+
|
|
35
|
+
# Test: XCUIElements exist
|
|
36
|
+
results["total"] += 1
|
|
37
|
+
try:
|
|
38
|
+
elements = self.driver.find_elements("class name", "XCUIElementTypeAny")
|
|
39
|
+
if len(elements) > 0:
|
|
40
|
+
results["passed"] += 1
|
|
41
|
+
else:
|
|
42
|
+
results["failed"] += 1
|
|
43
|
+
except Exception:
|
|
44
|
+
results["failed"] += 1
|
|
45
|
+
|
|
46
|
+
return results
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""System testing engines"""
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Load Testing Engine"""
|
|
2
|
+
|
|
3
|
+
from typing import Dict
|
|
4
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LoadTestEngine:
|
|
9
|
+
"""Load testing with configurable scenarios"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, settings, db, privacy):
|
|
12
|
+
self.settings = settings
|
|
13
|
+
self.db = db
|
|
14
|
+
self.privacy = privacy
|
|
15
|
+
|
|
16
|
+
def run_tests(self, target: str, scenario: str = "ramp",
|
|
17
|
+
max_users: int = 100, duration: int = 60) -> Dict:
|
|
18
|
+
"""Run load tests"""
|
|
19
|
+
results = {"total": 0, "passed": 0, "failed": 0, "metrics": {}}
|
|
20
|
+
|
|
21
|
+
if scenario == "ramp":
|
|
22
|
+
results = self._ramp_test(target, max_users, duration)
|
|
23
|
+
elif scenario == "constant":
|
|
24
|
+
results = self._constant_test(target, max_users, duration)
|
|
25
|
+
elif scenario == "spike":
|
|
26
|
+
results = self._spike_test(target, max_users)
|
|
27
|
+
|
|
28
|
+
return results
|
|
29
|
+
|
|
30
|
+
def _ramp_test(self, target: str, max_users: int, duration: int) -> Dict:
|
|
31
|
+
"""Gradually increase load"""
|
|
32
|
+
return self._run_load_scenario(target, max_users, duration, "ramp")
|
|
33
|
+
|
|
34
|
+
def _constant_test(self, target: str, users: int, duration: int) -> Dict:
|
|
35
|
+
"""Constant load"""
|
|
36
|
+
return self._run_load_scenario(target, users, duration, "constant")
|
|
37
|
+
|
|
38
|
+
def _spike_test(self, target: str, users: int) -> Dict:
|
|
39
|
+
"""Sudden spike in load"""
|
|
40
|
+
return self._run_load_scenario(target, users, 10, "spike")
|
|
41
|
+
|
|
42
|
+
def _run_load_scenario(self, target: str, max_users: int, duration: int, scenario: str) -> Dict:
|
|
43
|
+
"""Execute a load test scenario"""
|
|
44
|
+
results = {
|
|
45
|
+
"total": 1, "passed": 0, "failed": 0,
|
|
46
|
+
"metrics": {
|
|
47
|
+
"scenario": scenario,
|
|
48
|
+
"max_users": max_users,
|
|
49
|
+
"duration": duration,
|
|
50
|
+
"total_requests": 0,
|
|
51
|
+
"successful": 0,
|
|
52
|
+
"failed": 0,
|
|
53
|
+
"avg_response_ms": 0,
|
|
54
|
+
"p95_ms": 0,
|
|
55
|
+
"p99_ms": 0,
|
|
56
|
+
"requests_per_sec": 0,
|
|
57
|
+
"error_rate": 0,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
import httpx
|
|
63
|
+
|
|
64
|
+
all_times = []
|
|
65
|
+
success_count = 0
|
|
66
|
+
fail_count = 0
|
|
67
|
+
start_time = time.time()
|
|
68
|
+
|
|
69
|
+
def make_request(_):
|
|
70
|
+
try:
|
|
71
|
+
s = time.time()
|
|
72
|
+
resp = httpx.get(target, timeout=10)
|
|
73
|
+
elapsed = time.time() - s
|
|
74
|
+
return (resp.status_code < 500, elapsed)
|
|
75
|
+
except Exception:
|
|
76
|
+
return (False, 0)
|
|
77
|
+
|
|
78
|
+
with ThreadPoolExecutor(max_workers=min(max_users, 50)) as executor:
|
|
79
|
+
futures = [executor.submit(make_request, i) for i in range(max_users * 3)]
|
|
80
|
+
|
|
81
|
+
for f in futures:
|
|
82
|
+
success, elapsed = f.result()
|
|
83
|
+
if success:
|
|
84
|
+
success_count += 1
|
|
85
|
+
else:
|
|
86
|
+
fail_count += 1
|
|
87
|
+
if elapsed > 0:
|
|
88
|
+
all_times.append(elapsed)
|
|
89
|
+
|
|
90
|
+
total_time = time.time() - start_time
|
|
91
|
+
total_req = success_count + fail_count
|
|
92
|
+
|
|
93
|
+
sorted_times = sorted(all_times)
|
|
94
|
+
p95_idx = int(len(sorted_times) * 0.95) if sorted_times else 0
|
|
95
|
+
p99_idx = int(len(sorted_times) * 0.99) if sorted_times else 0
|
|
96
|
+
|
|
97
|
+
results["metrics"] = {
|
|
98
|
+
"scenario": scenario,
|
|
99
|
+
"max_users": max_users,
|
|
100
|
+
"duration": duration,
|
|
101
|
+
"total_requests": total_req,
|
|
102
|
+
"successful": success_count,
|
|
103
|
+
"failed": fail_count,
|
|
104
|
+
"avg_response_ms": round(sum(all_times) / len(all_times) * 1000, 2) if all_times else 0,
|
|
105
|
+
"p95_ms": round(sorted_times[p95_idx] * 1000, 2) if sorted_times else 0,
|
|
106
|
+
"p99_ms": round(sorted_times[p99_idx] * 1000, 2) if sorted_times else 0,
|
|
107
|
+
"requests_per_sec": round(total_req / total_time, 2) if total_time > 0 else 0,
|
|
108
|
+
"error_rate": round((fail_count / total_req * 100), 2) if total_req > 0 else 0,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Pass if error rate < 5%
|
|
112
|
+
if results["metrics"]["error_rate"] < 5:
|
|
113
|
+
results["passed"] = 1
|
|
114
|
+
else:
|
|
115
|
+
results["failed"] = 1
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
results["failed"] = 1
|
|
119
|
+
results["metrics"]["error"] = str(e)
|
|
120
|
+
|
|
121
|
+
return results
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Performance Testing Engine - Load, stress, and benchmark testing
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
import asyncio
|
|
7
|
+
from typing import Dict, List
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
9
|
+
import threading
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PerformanceTestEngine:
|
|
13
|
+
"""Performance, load, and stress testing"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, settings, db, privacy):
|
|
16
|
+
self.settings = settings
|
|
17
|
+
self.db = db
|
|
18
|
+
self.privacy = privacy
|
|
19
|
+
|
|
20
|
+
def run_tests(self, target: str, concurrent_users: int = 10,
|
|
21
|
+
duration: int = 30, ramp_up: int = 5) -> Dict:
|
|
22
|
+
"""Run performance tests"""
|
|
23
|
+
results = {"total": 0, "passed": 0, "failed": 0, "metrics": {}}
|
|
24
|
+
|
|
25
|
+
# Phase 1: Baseline Performance
|
|
26
|
+
baseline = self._test_baseline(target)
|
|
27
|
+
results["metrics"]["baseline"] = baseline
|
|
28
|
+
|
|
29
|
+
# Phase 2: Load Test
|
|
30
|
+
load = self._test_load(target, concurrent_users, duration)
|
|
31
|
+
results["metrics"]["load"] = load
|
|
32
|
+
|
|
33
|
+
# Phase 3: Stress Test
|
|
34
|
+
stress = self._test_stress(target, concurrent_users * 5)
|
|
35
|
+
results["metrics"]["stress"] = stress
|
|
36
|
+
|
|
37
|
+
# Phase 4: Spike Test
|
|
38
|
+
spike = self._test_spike(target, concurrent_users * 10)
|
|
39
|
+
results["metrics"]["spike"] = spike
|
|
40
|
+
|
|
41
|
+
# Calculate overall
|
|
42
|
+
results["total"] = 4
|
|
43
|
+
passed = sum(1 for k, v in results["metrics"].items() if v.get("status") == "pass")
|
|
44
|
+
results["passed"] = passed
|
|
45
|
+
results["failed"] = results["total"] - passed
|
|
46
|
+
|
|
47
|
+
if results["total"] > 0:
|
|
48
|
+
results["coverage"] = round((results["passed"] / results["total"]) * 100, 2)
|
|
49
|
+
|
|
50
|
+
return results
|
|
51
|
+
|
|
52
|
+
def _test_baseline(self, target: str) -> Dict:
|
|
53
|
+
"""Test baseline response time"""
|
|
54
|
+
try:
|
|
55
|
+
import httpx
|
|
56
|
+
|
|
57
|
+
times = []
|
|
58
|
+
for _ in range(10):
|
|
59
|
+
start = time.time()
|
|
60
|
+
resp = httpx.get(target, timeout=10)
|
|
61
|
+
elapsed = time.time() - start
|
|
62
|
+
times.append(elapsed)
|
|
63
|
+
|
|
64
|
+
avg = sum(times) / len(times)
|
|
65
|
+
p95 = sorted(times)[int(len(times) * 0.95)]
|
|
66
|
+
p99 = sorted(times)[int(len(times) * 0.99)]
|
|
67
|
+
|
|
68
|
+
status = "pass" if avg < 2.0 else "fail"
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
"avg_ms": round(avg * 1000, 2),
|
|
72
|
+
"p95_ms": round(p95 * 1000, 2),
|
|
73
|
+
"p99_ms": round(p99 * 1000, 2),
|
|
74
|
+
"min_ms": round(min(times) * 1000, 2),
|
|
75
|
+
"max_ms": round(max(times) * 1000, 2),
|
|
76
|
+
"status": status,
|
|
77
|
+
}
|
|
78
|
+
except Exception as e:
|
|
79
|
+
return {"status": "fail", "error": str(e)}
|
|
80
|
+
|
|
81
|
+
def _test_load(self, target: str, users: int, duration: int) -> Dict:
|
|
82
|
+
"""Simulate concurrent load"""
|
|
83
|
+
try:
|
|
84
|
+
import httpx
|
|
85
|
+
|
|
86
|
+
results = {"success": 0, "failed": 0, "total_time": 0, "times": []}
|
|
87
|
+
|
|
88
|
+
def make_request(_):
|
|
89
|
+
try:
|
|
90
|
+
start = time.time()
|
|
91
|
+
resp = httpx.get(target, timeout=10)
|
|
92
|
+
elapsed = time.time() - start
|
|
93
|
+
return {"success": resp.status_code < 500, "time": elapsed}
|
|
94
|
+
except Exception:
|
|
95
|
+
return {"success": False, "time": 0}
|
|
96
|
+
|
|
97
|
+
with ThreadPoolExecutor(max_workers=users) as executor:
|
|
98
|
+
futures = [executor.submit(make_request, i) for i in range(users * 3)]
|
|
99
|
+
|
|
100
|
+
for future in futures:
|
|
101
|
+
result = future.result()
|
|
102
|
+
if result["success"]:
|
|
103
|
+
results["success"] += 1
|
|
104
|
+
else:
|
|
105
|
+
results["failed"] += 1
|
|
106
|
+
results["times"].append(result["time"])
|
|
107
|
+
|
|
108
|
+
total = results["success"] + results["failed"]
|
|
109
|
+
success_rate = (results["success"] / total * 100) if total > 0 else 0
|
|
110
|
+
|
|
111
|
+
status = "pass" if success_rate >= 95 else "fail"
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
"total_requests": total,
|
|
115
|
+
"success_rate": round(success_rate, 2),
|
|
116
|
+
"avg_response_ms": round(sum(results["times"]) / len(results["times"]) * 1000, 2) if results["times"] else 0,
|
|
117
|
+
"status": status,
|
|
118
|
+
}
|
|
119
|
+
except Exception as e:
|
|
120
|
+
return {"status": "fail", "error": str(e)}
|
|
121
|
+
|
|
122
|
+
def _test_stress(self, target: str, users: int) -> Dict:
|
|
123
|
+
"""Stress test with high concurrency"""
|
|
124
|
+
return self._test_load(target, users, 10)
|
|
125
|
+
|
|
126
|
+
def _test_spike(self, target: str, users: int) -> Dict:
|
|
127
|
+
"""Spike test - sudden burst of requests"""
|
|
128
|
+
return self._test_load(target, users, 5)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Security Testing Engine"""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SecurityTestEngine:
|
|
8
|
+
"""Security and vulnerability testing"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, settings, db, privacy):
|
|
11
|
+
self.settings = settings
|
|
12
|
+
self.db = db
|
|
13
|
+
self.privacy = privacy
|
|
14
|
+
|
|
15
|
+
def run_tests(self, target: str, scan_type: str = "basic") -> Dict:
|
|
16
|
+
"""Run security tests"""
|
|
17
|
+
results = {"total": 0, "passed": 0, "failed": 0, "vulnerabilities": []}
|
|
18
|
+
|
|
19
|
+
# Test: SSL/TLS
|
|
20
|
+
results["total"] += 1
|
|
21
|
+
ssl_ok = self._check_ssl(target)
|
|
22
|
+
if ssl_ok:
|
|
23
|
+
results["passed"] += 1
|
|
24
|
+
else:
|
|
25
|
+
results["failed"] += 1
|
|
26
|
+
results["vulnerabilities"].append({"type": "SSL", "severity": "high", "detail": "SSL certificate issue"})
|
|
27
|
+
|
|
28
|
+
# Test: Security Headers
|
|
29
|
+
results["total"] += 1
|
|
30
|
+
headers_ok = self._check_security_headers(target)
|
|
31
|
+
if headers_ok:
|
|
32
|
+
results["passed"] += 1
|
|
33
|
+
else:
|
|
34
|
+
results["failed"] += 1
|
|
35
|
+
results["vulnerabilities"].append({"type": "Headers", "severity": "medium", "detail": "Missing security headers"})
|
|
36
|
+
|
|
37
|
+
# Test: Open Ports
|
|
38
|
+
results["total"] += 1
|
|
39
|
+
ports_ok = self._check_open_ports(target)
|
|
40
|
+
if ports_ok:
|
|
41
|
+
results["passed"] += 1
|
|
42
|
+
else:
|
|
43
|
+
results["failed"] += 1
|
|
44
|
+
results["vulnerabilities"].append({"type": "Ports", "severity": "medium", "detail": "Unexpected open ports"})
|
|
45
|
+
|
|
46
|
+
# Test: SQL Injection (basic)
|
|
47
|
+
results["total"] += 1
|
|
48
|
+
sqli_ok = self._check_sqli(target)
|
|
49
|
+
if sqli_ok:
|
|
50
|
+
results["passed"] += 1
|
|
51
|
+
else:
|
|
52
|
+
results["failed"] += 1
|
|
53
|
+
results["vulnerabilities"].append({"type": "SQLi", "severity": "critical", "detail": "Possible SQL injection"})
|
|
54
|
+
|
|
55
|
+
# Test: XSS (basic)
|
|
56
|
+
results["total"] += 1
|
|
57
|
+
xss_ok = self._check_xss(target)
|
|
58
|
+
if xss_ok:
|
|
59
|
+
results["passed"] += 1
|
|
60
|
+
else:
|
|
61
|
+
results["failed"] += 1
|
|
62
|
+
results["vulnerabilities"].append({"type": "XSS", "severity": "high", "detail": "Possible XSS vulnerability"})
|
|
63
|
+
|
|
64
|
+
# Test: CORS Configuration
|
|
65
|
+
results["total"] += 1
|
|
66
|
+
cors_ok = self._check_cors(target)
|
|
67
|
+
if cors_ok:
|
|
68
|
+
results["passed"] += 1
|
|
69
|
+
else:
|
|
70
|
+
results["failed"] += 1
|
|
71
|
+
results["vulnerabilities"].append({"type": "CORS", "severity": "medium", "detail": "Overly permissive CORS"})
|
|
72
|
+
|
|
73
|
+
if results["total"] > 0:
|
|
74
|
+
results["coverage"] = round((results["passed"] / results["total"]) * 100, 2)
|
|
75
|
+
|
|
76
|
+
return results
|
|
77
|
+
|
|
78
|
+
def _check_ssl(self, target: str) -> bool:
|
|
79
|
+
"""Check SSL/TLS configuration"""
|
|
80
|
+
try:
|
|
81
|
+
import ssl
|
|
82
|
+
import socket
|
|
83
|
+
|
|
84
|
+
hostname = target.replace("http://", "").replace("https://", "").split("/")[0].split(":")[0]
|
|
85
|
+
context = ssl.create_default_context()
|
|
86
|
+
|
|
87
|
+
with socket.create_connection((hostname, 443), timeout=5) as sock:
|
|
88
|
+
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
|
|
89
|
+
cert = ssock.getpeercert()
|
|
90
|
+
return cert is not None
|
|
91
|
+
except Exception:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
def _check_security_headers(self, target: str) -> bool:
|
|
95
|
+
"""Check for security headers"""
|
|
96
|
+
try:
|
|
97
|
+
import httpx
|
|
98
|
+
resp = httpx.get(target, timeout=10)
|
|
99
|
+
|
|
100
|
+
required = ["x-content-type-options", "x-frame-options"]
|
|
101
|
+
present = sum(1 for h in required if h in resp.headers)
|
|
102
|
+
return present >= 1
|
|
103
|
+
except Exception:
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
def _check_open_ports(self, target: str) -> bool:
|
|
107
|
+
"""Check for unexpected open ports"""
|
|
108
|
+
try:
|
|
109
|
+
import socket
|
|
110
|
+
hostname = target.replace("http://", "").replace("https://", "").split("/")[0].split(":")[0]
|
|
111
|
+
|
|
112
|
+
dangerous_ports = [21, 23, 3389, 5900] # FTP, Telnet, RDP, VNC
|
|
113
|
+
open_dangerous = []
|
|
114
|
+
|
|
115
|
+
for port in dangerous_ports:
|
|
116
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
117
|
+
sock.settimeout(2)
|
|
118
|
+
result = sock.connect_ex((hostname, port))
|
|
119
|
+
sock.close()
|
|
120
|
+
if result == 0:
|
|
121
|
+
open_dangerous.append(port)
|
|
122
|
+
|
|
123
|
+
return len(open_dangerous) == 0
|
|
124
|
+
except Exception:
|
|
125
|
+
return True # Assume safe if can't check
|
|
126
|
+
|
|
127
|
+
def _check_sqli(self, target: str) -> bool:
|
|
128
|
+
"""Basic SQL injection check"""
|
|
129
|
+
try:
|
|
130
|
+
import httpx
|
|
131
|
+
# Test with common SQL injection payloads
|
|
132
|
+
payloads = ["' OR 1=1 --", "'; DROP TABLE users --"]
|
|
133
|
+
|
|
134
|
+
for payload in payloads:
|
|
135
|
+
resp = httpx.get(f"{target}?id={payload}", timeout=10)
|
|
136
|
+
# Check for SQL error messages in response
|
|
137
|
+
sql_errors = ["sql", "syntax", "mysql", "postgres", "sqlite", "oracle"]
|
|
138
|
+
content = resp.text.lower()
|
|
139
|
+
if any(err in content for err in sql_errors):
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
return True
|
|
143
|
+
except Exception:
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
def _check_xss(self, target: str) -> bool:
|
|
147
|
+
"""Basic XSS check"""
|
|
148
|
+
try:
|
|
149
|
+
import httpx
|
|
150
|
+
payload = "<script>alert('xss')</script>"
|
|
151
|
+
resp = httpx.get(f"{target}?q={payload}", timeout=10)
|
|
152
|
+
|
|
153
|
+
# If payload is reflected without encoding, potential XSS
|
|
154
|
+
if payload in resp.text:
|
|
155
|
+
return False
|
|
156
|
+
return True
|
|
157
|
+
except Exception:
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
def _check_cors(self, target: str) -> bool:
|
|
161
|
+
"""Check CORS configuration"""
|
|
162
|
+
try:
|
|
163
|
+
import httpx
|
|
164
|
+
resp = httpx.options(target, headers={"Origin": "http://evil.com"}, timeout=10)
|
|
165
|
+
cors = resp.headers.get("access-control-allow-origin", "")
|
|
166
|
+
|
|
167
|
+
# Should not allow all origins
|
|
168
|
+
return cors != "*"
|
|
169
|
+
except Exception:
|
|
170
|
+
return True
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Web testing engines"""
|