hackable 1.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.
Files changed (35) hide show
  1. hackable-1.0.0/LICENSE +21 -0
  2. hackable-1.0.0/PKG-INFO +104 -0
  3. hackable-1.0.0/README.md +84 -0
  4. hackable-1.0.0/pyproject.toml +31 -0
  5. hackable-1.0.0/setup.cfg +4 -0
  6. hackable-1.0.0/src/hackable/__init__.py +3 -0
  7. hackable-1.0.0/src/hackable/__main__.py +4 -0
  8. hackable-1.0.0/src/hackable/checks/__init__.py +22 -0
  9. hackable-1.0.0/src/hackable/checks/cookies.py +65 -0
  10. hackable-1.0.0/src/hackable/checks/cors.py +68 -0
  11. hackable-1.0.0/src/hackable/checks/debug.py +43 -0
  12. hackable-1.0.0/src/hackable/checks/disclosure.py +36 -0
  13. hackable-1.0.0/src/hackable/checks/exposed_files.py +122 -0
  14. hackable-1.0.0/src/hackable/checks/headers.py +70 -0
  15. hackable-1.0.0/src/hackable/checks/methods.py +28 -0
  16. hackable-1.0.0/src/hackable/checks/open_redirect.py +57 -0
  17. hackable-1.0.0/src/hackable/checks/rate_limit.py +53 -0
  18. hackable-1.0.0/src/hackable/checks/robots.py +31 -0
  19. hackable-1.0.0/src/hackable/checks/securitytxt.py +21 -0
  20. hackable-1.0.0/src/hackable/checks/sqli.py +152 -0
  21. hackable-1.0.0/src/hackable/checks/tls.py +89 -0
  22. hackable-1.0.0/src/hackable/checks/xss.py +83 -0
  23. hackable-1.0.0/src/hackable/cli.py +117 -0
  24. hackable-1.0.0/src/hackable/crawler.py +98 -0
  25. hackable-1.0.0/src/hackable/findings.py +43 -0
  26. hackable-1.0.0/src/hackable/polite.py +47 -0
  27. hackable-1.0.0/src/hackable/report.py +244 -0
  28. hackable-1.0.0/src/hackable/scanner.py +60 -0
  29. hackable-1.0.0/src/hackable.egg-info/PKG-INFO +104 -0
  30. hackable-1.0.0/src/hackable.egg-info/SOURCES.txt +33 -0
  31. hackable-1.0.0/src/hackable.egg-info/dependency_links.txt +1 -0
  32. hackable-1.0.0/src/hackable.egg-info/entry_points.txt +2 -0
  33. hackable-1.0.0/src/hackable.egg-info/requires.txt +1 -0
  34. hackable-1.0.0/src/hackable.egg-info/top_level.txt +1 -0
  35. hackable-1.0.0/tests/test_checks.py +296 -0
hackable-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shafqat Mehmood
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,104 @@
1
+ Metadata-Version: 2.4
2
+ Name: hackable
3
+ Version: 1.0.0
4
+ Summary: Hack yourself before they do. One-command website security check for people who have never run a security tool.
5
+ Author: Shafqat Mehmood
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Shifu34/hackable
8
+ Keywords: security,pentest,scanner,web,vulnerability,dast
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Security
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: requests
19
+ Dynamic: license-file
20
+
21
+ # hackable
22
+
23
+ **Hack yourself before they do.**
24
+
25
+ One-command website security check for people who have never run a security tool. Point it at your app, and in about ten seconds it tells you, in plain English, how a hacker would break in and exactly how to stop them.
26
+
27
+ ```
28
+ $ pip install hackable
29
+ $ hackable https://myapp.com
30
+ ```
31
+
32
+ ![hackable scanning a deliberately vulnerable demo app](demo/demo.gif)
33
+
34
+ ## The problem
35
+
36
+ Millions of apps are being built with AI right now by people who have never heard of OWASP. The apps work. They also leak database keys, take SQL injection, and let anyone log in as anyone. The existing scanners (ZAP, Nuclei, Burp) are expert tools with expert UX. They might as well be in another language.
37
+
38
+ hackable is the Let's Encrypt of pentesting: free, one command, plain English.
39
+
40
+ ## What it checks
41
+
42
+ 14 checks. Before probing, hackable crawls your app like a visitor would,
43
+ collecting real links and forms, so injection tests hit your actual inputs
44
+ instead of guessed parameter names.
45
+
46
+ | Check | What it finds |
47
+ |---|---|
48
+ | SQL injection | Error-based probes plus boolean differential checks (safe, GET-only) |
49
+ | Exposed files | Public `/.env`, `/.git/`, config backups, with content verification (no false alarms from SPA fallbacks) |
50
+ | XSS | Your input reflected unescaped into the page |
51
+ | Open redirects | Your site redirecting visitors to attacker domains |
52
+ | CORS | Arbitrary origins trusted with credentials |
53
+ | Debug mode | Stack traces leaking from error pages |
54
+ | Login rate limiting | 10 rapid wrong passwords, checking for pushback |
55
+ | Security headers | HSTS, CSP, X-Frame-Options, and friends |
56
+ | Cookie flags | Session cookies missing Secure, HttpOnly, or SameSite |
57
+ | TLS | Missing HTTPS, expired certs, ancient TLS versions |
58
+ | Version disclosure | Server headers announcing exact versions |
59
+ | HTTP methods | Risky methods like TRACE |
60
+ | robots.txt | Hidden paths handed to attackers on a plate |
61
+ | security.txt | No standard contact for researchers to report issues |
62
+
63
+ Every finding comes with **what this means** (one sentence, no jargon) and **how to fix it** (one sentence, actionable). You get a 0-100 score and an A-F grade.
64
+
65
+ ## Output
66
+
67
+ Terminal report, machine-readable JSON (`--json`), SARIF 2.1.0 for GitHub code
68
+ scanning (`--sarif`), and a self-contained shareable HTML report card
69
+ (`--html report.html`).
70
+
71
+ ```bash
72
+ hackable https://myapp.com --html report.html
73
+ hackable https://myapp.com --fail-under 70 # exit 1 in CI if score < 70
74
+ hackable https://myapp.com --only sqli,xss # run a subset of checks
75
+ ```
76
+
77
+ ## GitHub Action
78
+
79
+ Drop it into your workflow and every push gets scanned:
80
+
81
+ ```yaml
82
+ - uses: Shifu34/hackable@v1
83
+ with:
84
+ target: https://myapp.com
85
+ fail-under: 70
86
+ ```
87
+
88
+ Results upload to the Security tab automatically.
89
+
90
+ ## Safety
91
+
92
+ hackable only sends harmless probes: no destructive payloads, no data deletion, no login attempts with real credentials, ~50-150 requests with delays and an identifying User-Agent. It will ask for confirmation before scanning unless you pass `--yes`.
93
+
94
+ **Only scan apps you own or have explicit permission to test.** Scanning without permission may be illegal.
95
+
96
+ ## Roadmap
97
+
98
+ - Authenticated scans (test behind a login)
99
+ - JavaScript-rendered app support
100
+ - Scheduled scans with diff alerts
101
+
102
+ ## License
103
+
104
+ MIT
@@ -0,0 +1,84 @@
1
+ # hackable
2
+
3
+ **Hack yourself before they do.**
4
+
5
+ One-command website security check for people who have never run a security tool. Point it at your app, and in about ten seconds it tells you, in plain English, how a hacker would break in and exactly how to stop them.
6
+
7
+ ```
8
+ $ pip install hackable
9
+ $ hackable https://myapp.com
10
+ ```
11
+
12
+ ![hackable scanning a deliberately vulnerable demo app](demo/demo.gif)
13
+
14
+ ## The problem
15
+
16
+ Millions of apps are being built with AI right now by people who have never heard of OWASP. The apps work. They also leak database keys, take SQL injection, and let anyone log in as anyone. The existing scanners (ZAP, Nuclei, Burp) are expert tools with expert UX. They might as well be in another language.
17
+
18
+ hackable is the Let's Encrypt of pentesting: free, one command, plain English.
19
+
20
+ ## What it checks
21
+
22
+ 14 checks. Before probing, hackable crawls your app like a visitor would,
23
+ collecting real links and forms, so injection tests hit your actual inputs
24
+ instead of guessed parameter names.
25
+
26
+ | Check | What it finds |
27
+ |---|---|
28
+ | SQL injection | Error-based probes plus boolean differential checks (safe, GET-only) |
29
+ | Exposed files | Public `/.env`, `/.git/`, config backups, with content verification (no false alarms from SPA fallbacks) |
30
+ | XSS | Your input reflected unescaped into the page |
31
+ | Open redirects | Your site redirecting visitors to attacker domains |
32
+ | CORS | Arbitrary origins trusted with credentials |
33
+ | Debug mode | Stack traces leaking from error pages |
34
+ | Login rate limiting | 10 rapid wrong passwords, checking for pushback |
35
+ | Security headers | HSTS, CSP, X-Frame-Options, and friends |
36
+ | Cookie flags | Session cookies missing Secure, HttpOnly, or SameSite |
37
+ | TLS | Missing HTTPS, expired certs, ancient TLS versions |
38
+ | Version disclosure | Server headers announcing exact versions |
39
+ | HTTP methods | Risky methods like TRACE |
40
+ | robots.txt | Hidden paths handed to attackers on a plate |
41
+ | security.txt | No standard contact for researchers to report issues |
42
+
43
+ Every finding comes with **what this means** (one sentence, no jargon) and **how to fix it** (one sentence, actionable). You get a 0-100 score and an A-F grade.
44
+
45
+ ## Output
46
+
47
+ Terminal report, machine-readable JSON (`--json`), SARIF 2.1.0 for GitHub code
48
+ scanning (`--sarif`), and a self-contained shareable HTML report card
49
+ (`--html report.html`).
50
+
51
+ ```bash
52
+ hackable https://myapp.com --html report.html
53
+ hackable https://myapp.com --fail-under 70 # exit 1 in CI if score < 70
54
+ hackable https://myapp.com --only sqli,xss # run a subset of checks
55
+ ```
56
+
57
+ ## GitHub Action
58
+
59
+ Drop it into your workflow and every push gets scanned:
60
+
61
+ ```yaml
62
+ - uses: Shifu34/hackable@v1
63
+ with:
64
+ target: https://myapp.com
65
+ fail-under: 70
66
+ ```
67
+
68
+ Results upload to the Security tab automatically.
69
+
70
+ ## Safety
71
+
72
+ hackable only sends harmless probes: no destructive payloads, no data deletion, no login attempts with real credentials, ~50-150 requests with delays and an identifying User-Agent. It will ask for confirmation before scanning unless you pass `--yes`.
73
+
74
+ **Only scan apps you own or have explicit permission to test.** Scanning without permission may be illegal.
75
+
76
+ ## Roadmap
77
+
78
+ - Authenticated scans (test behind a login)
79
+ - JavaScript-rendered app support
80
+ - Scheduled scans with diff alerts
81
+
82
+ ## License
83
+
84
+ MIT
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hackable"
7
+ version = "1.0.0"
8
+ description = "Hack yourself before they do. One-command website security check for people who have never run a security tool."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Shafqat Mehmood" }]
13
+ keywords = ["security", "pentest", "scanner", "web", "vulnerability", "dast"]
14
+ dependencies = ["requests"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: Security",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/Shifu34/hackable"
26
+
27
+ [project.scripts]
28
+ hackable = "hackable.cli:main"
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """hackable: hack yourself before they do."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,22 @@
1
+ """Security checks. Each exposes run(base, http) -> list[Finding].
2
+ Injection checks (sqli, xss, open_redirect) also accept an optional third
3
+ argument: discovered (url, [params]) pairs from the crawler."""
4
+
5
+ from . import cookies, cors, debug, disclosure, exposed_files, headers, methods, open_redirect, rate_limit, robots, securitytxt, sqli, tls, xss
6
+
7
+ CHECKS = [
8
+ ("headers", "security headers", headers.run),
9
+ ("robots", "robots.txt", robots.run),
10
+ ("disclosure", "version disclosure", disclosure.run),
11
+ ("exposed_files", "exposed sensitive files", exposed_files.run),
12
+ ("debug", "debug mode / stack traces", debug.run),
13
+ ("cors", "CORS misconfiguration", cors.run),
14
+ ("methods", "dangerous HTTP methods", methods.run),
15
+ ("cookies", "cookie security flags", cookies.run),
16
+ ("sqli", "SQL injection", sqli.run),
17
+ ("xss", "cross-site scripting", xss.run),
18
+ ("open_redirect", "open redirects", open_redirect.run),
19
+ ("rate_limit", "login rate limiting", rate_limit.run),
20
+ ("tls", "TLS / HTTPS", tls.run),
21
+ ("securitytxt", "security.txt", securitytxt.run),
22
+ ]
@@ -0,0 +1,65 @@
1
+ """Check that cookies, especially session cookies, carry security flags."""
2
+
3
+ from ..findings import Finding
4
+
5
+ FLAG_ADVICE = {
6
+ "secure": ("medium", "Cookie without the Secure flag",
7
+ "This cookie can be sent over plain HTTP, where anyone on the "
8
+ "network can read or steal it.",
9
+ "Fix: set the Secure attribute so the cookie is only ever "
10
+ "sent over HTTPS."),
11
+ "httponly": ("low", "Cookie without the HttpOnly flag",
12
+ "JavaScript running in the page can read this cookie, which "
13
+ "makes session theft via XSS much easier.",
14
+ "Fix: set the HttpOnly attribute so JavaScript cannot read "
15
+ "the cookie. This blunts XSS-based session theft."),
16
+ "samesite": ("low", "Cookie without the SameSite attribute",
17
+ "The browser sends this cookie on cross-site requests, which "
18
+ "enables cross-site request forgery attacks.",
19
+ "Fix: set SameSite=Lax (or Strict) to block the cookie on "
20
+ "cross-site requests and reduce CSRF risk."),
21
+ }
22
+
23
+
24
+ def _raw_cookies(response):
25
+ try:
26
+ return response.raw.headers.getlist("set-cookie")
27
+ except Exception:
28
+ pass
29
+ single = response.headers.get("set-cookie")
30
+ return [single] if single else []
31
+
32
+
33
+ def run(base, http):
34
+ r = http.get(base + "/")
35
+ if r is None:
36
+ return []
37
+ raw = _raw_cookies(r)
38
+ if not raw:
39
+ return []
40
+
41
+ is_https = base.startswith("https://")
42
+ missing = {"secure": [], "httponly": [], "samesite": []}
43
+ for cookie in raw:
44
+ parts = [p.strip() for p in cookie.split(";")]
45
+ name = parts[0].split("=", 1)[0] or "(unnamed)"
46
+ flags = {p.split("=", 1)[0].lower() for p in parts[1:]}
47
+ for flag in missing:
48
+ if flag == "secure" and not is_https:
49
+ continue # Secure only makes sense over HTTPS
50
+ if flag not in flags:
51
+ missing[flag].append(name)
52
+
53
+ findings = []
54
+ for flag, names in missing.items():
55
+ if not names:
56
+ continue
57
+ severity, title, meaning, fix = FLAG_ADVICE[flag]
58
+ findings.append(Finding(
59
+ check="cookies", severity=severity,
60
+ title=f"{title}: {', '.join(names[:3])}",
61
+ meaning=meaning,
62
+ url=base + "/", evidence=f"Set-Cookie flags seen: {flag} missing",
63
+ fix=fix,
64
+ ))
65
+ return findings
@@ -0,0 +1,68 @@
1
+ """CORS misconfiguration."""
2
+
3
+ from ..findings import Finding
4
+
5
+ EVIL = "https://evil-hackable-test.example.com"
6
+
7
+
8
+ def _check_response(r):
9
+ acao = r.headers.get("Access-Control-Allow-Origin", "")
10
+ acac = r.headers.get("Access-Control-Allow-Credentials", "").lower() == "true"
11
+ if acao == EVIL and acac:
12
+ return "critical"
13
+ if acao == EVIL or (acao == "*" and acac):
14
+ return "high"
15
+ if acao == "*":
16
+ return "info"
17
+ return None
18
+
19
+
20
+ def run(base, http):
21
+ r = http.options(base + "/", headers={"Origin": EVIL})
22
+ verdict = _check_response(r) if r is not None else None
23
+ if verdict is None:
24
+ r = http.get(base + "/", headers={"Origin": EVIL})
25
+ verdict = _check_response(r) if r is not None else None
26
+ if verdict is None:
27
+ return []
28
+ if verdict == "critical":
29
+ return [
30
+ Finding(
31
+ check="cors",
32
+ severity="critical",
33
+ title="Any website can make authenticated requests as your users",
34
+ meaning="Your server tells browsers that evil-hackable-test.example.com "
35
+ "may read responses AND send the victim's cookies along. A malicious "
36
+ "site can silently act as your logged-in users.",
37
+ fix="Never reflect arbitrary origins with Access-Control-Allow-Credentials. "
38
+ "Use an explicit allowlist of your own domains.",
39
+ evidence="Access-Control-Allow-Origin: %s + Allow-Credentials: true" % EVIL,
40
+ url=base + "/",
41
+ )
42
+ ]
43
+ if verdict == "high":
44
+ return [
45
+ Finding(
46
+ check="cors",
47
+ severity="high",
48
+ title="CORS trusts arbitrary websites",
49
+ meaning="Your server accepts cross-origin requests from any site I "
50
+ "named. Depending on your auth model, a malicious page could read "
51
+ "private API responses.",
52
+ fix="Replace the wildcard/reflected origin with an explicit allowlist "
53
+ "of domains you own.",
54
+ url=base + "/",
55
+ )
56
+ ]
57
+ return [
58
+ Finding(
59
+ check="cors",
60
+ severity="info",
61
+ title="CORS allows any origin (no credentials)",
62
+ meaning="Any website can read your public API responses. That is fine for "
63
+ "public data, but double-check nothing private is served this way.",
64
+ fix="If all responses are public, no action needed. Otherwise restrict "
65
+ "Access-Control-Allow-Origin to your own domains.",
66
+ url=base + "/",
67
+ )
68
+ ]
@@ -0,0 +1,43 @@
1
+ """Debug mode / stack traces exposed on error pages."""
2
+
3
+ import secrets
4
+
5
+ from ..findings import Finding
6
+
7
+ MARKERS = [
8
+ "traceback (most recent call last)",
9
+ "django.core.exceptions",
10
+ "werkzeug.debug",
11
+ "raise valueerror",
12
+ 'at ".rb:',
13
+ ".java:",
14
+ "system.nullreferenceexception",
15
+ "stack trace:",
16
+ "#0 ",
17
+ "fatal error:",
18
+ ]
19
+
20
+
21
+ def run(base, http):
22
+ probe = "/hackable-probe-" + secrets.token_hex(4)
23
+ r = http.get(base + probe)
24
+ if r is None:
25
+ return []
26
+ body = r.text.lower()
27
+ hits = [m for m in MARKERS if m in body]
28
+ if not hits:
29
+ return []
30
+ return [
31
+ Finding(
32
+ check="debug",
33
+ severity="high",
34
+ title="Error pages leak stack traces",
35
+ meaning="Your 404 page shows internal error details (matched: %s). Stack "
36
+ "traces reveal file paths, library versions and code structure, which "
37
+ "is exactly what an attacker needs to aim." % ", ".join(hits[:3]),
38
+ fix="Turn off debug mode in production and serve a generic error page "
39
+ "that reveals nothing internal. Log the details server-side instead.",
40
+ evidence=r.text.strip().replace("\n", " ")[:160],
41
+ url=base + probe,
42
+ )
43
+ ]
@@ -0,0 +1,36 @@
1
+ """Software version disclosure in response headers."""
2
+
3
+ import re
4
+
5
+ from ..findings import Finding
6
+
7
+ VERSION_RE = re.compile(r"\d+\.\d+")
8
+
9
+ INTERESTING = ("server", "x-powered-by", "x-aspnet-version", "x-generator")
10
+
11
+
12
+ def run(base, http):
13
+ r = http.get(base + "/")
14
+ if r is None:
15
+ return []
16
+ leaked = []
17
+ for name in INTERESTING:
18
+ value = r.headers.get(name)
19
+ if value and VERSION_RE.search(value):
20
+ leaked.append("%s: %s" % (name, value.strip()))
21
+ if not leaked:
22
+ return []
23
+ return [
24
+ Finding(
25
+ check="disclosure",
26
+ severity="low",
27
+ title="Server software versions are public",
28
+ meaning="Your server announces exactly what it runs (%s). Attackers search "
29
+ "for old versions with known holes, so this tells them where to aim."
30
+ % "; ".join(leaked),
31
+ fix="Hide or genericize the Server and X-Powered-By headers in your web "
32
+ "server config. More importantly, keep the software itself updated.",
33
+ evidence="; ".join(leaked),
34
+ url=base + "/",
35
+ )
36
+ ]
@@ -0,0 +1,122 @@
1
+ """Sensitive files accidentally left public."""
2
+
3
+ from ..findings import Finding
4
+
5
+
6
+ def _looks_like_env(text):
7
+ t = text.strip()
8
+ if "<html" in t.lower()[:500]:
9
+ return False
10
+ return "=" in t and any(
11
+ k in t for k in ("KEY", "SECRET", "PASSWORD", "TOKEN", "DATABASE_URL")
12
+ )
13
+
14
+
15
+ def _looks_like_git_head(text):
16
+ return text.strip().startswith("ref:")
17
+
18
+
19
+ def _looks_like_git_config(text):
20
+ return "[core]" in text and "repositoryformatversion" in text
21
+
22
+
23
+ def _looks_like_apache_status(text):
24
+ return "Apache Status" in text
25
+
26
+
27
+ def _looks_like_wp_config(text):
28
+ return "DB_PASSWORD" in text
29
+
30
+
31
+ def _looks_like_ds_store(raw):
32
+ return raw[:8].startswith(b"\x00\x01Bud") or raw[:4] == b"Bud1"
33
+
34
+
35
+ TARGETS = [
36
+ (
37
+ "/.env",
38
+ "critical",
39
+ lambda text, raw: _looks_like_env(text),
40
+ "Your /.env file is public",
41
+ "This file usually holds database passwords, API keys and app secrets. "
42
+ "Anyone on the internet can now read them and log in as you.",
43
+ "Block /.env (and /.env.*) in your web server config immediately, then "
44
+ "rotate every secret that was inside it. They are all compromised.",
45
+ ),
46
+ (
47
+ "/.env.bak",
48
+ "critical",
49
+ lambda text, raw: _looks_like_env(text),
50
+ "A backup of your .env file is public",
51
+ "Same as a public /.env: every secret in it is now in attacker hands.",
52
+ "Delete the backup file from the server and rotate every secret in it.",
53
+ ),
54
+ (
55
+ "/.git/HEAD",
56
+ "critical",
57
+ lambda text, raw: _looks_like_git_head(text),
58
+ "Your .git directory is public",
59
+ "Attackers can download your entire source code, including old commits "
60
+ "that may contain secrets you later deleted.",
61
+ "Block /.git/ in your web server config or stop deploying the .git "
62
+ "directory to production.",
63
+ ),
64
+ (
65
+ "/.git/config",
66
+ "high",
67
+ lambda text, raw: _looks_like_git_config(text),
68
+ "Your git config is public",
69
+ "This confirms your repository internals are exposed and often leads to "
70
+ "full source disclosure.",
71
+ "Block /.git/ in your web server config.",
72
+ ),
73
+ (
74
+ "/server-status",
75
+ "high",
76
+ lambda text, raw: _looks_like_apache_status(text),
77
+ "Apache server-status page is public",
78
+ "It shows live requests, client IPs and server internals. Useful for you, "
79
+ "more useful for an attacker mapping your app.",
80
+ "Restrict /server-status to localhost in your Apache config.",
81
+ ),
82
+ (
83
+ "/wp-config.php.bak",
84
+ "critical",
85
+ lambda text, raw: _looks_like_wp_config(text),
86
+ "A WordPress config backup is public",
87
+ "It contains your database username and password in plain text.",
88
+ "Delete the backup file and change the database password.",
89
+ ),
90
+ (
91
+ "/.DS_Store",
92
+ "low",
93
+ lambda text, raw: _looks_like_ds_store(raw),
94
+ ".DS_Store file is public",
95
+ "A macOS metadata file. It can reveal file and folder names on your server.",
96
+ "Stop deploying .DS_Store files; add them to .gitignore.",
97
+ ),
98
+ ]
99
+
100
+
101
+ def run(base, http):
102
+ findings = []
103
+ for path, severity, signature, title, meaning, fix in TARGETS:
104
+ r = http.get(base + path)
105
+ if r is None or r.status_code != 200:
106
+ continue
107
+ try:
108
+ if signature(r.text, r.content):
109
+ findings.append(
110
+ Finding(
111
+ check="exposed_files",
112
+ severity=severity,
113
+ title=title,
114
+ meaning=meaning,
115
+ fix=fix,
116
+ evidence=r.text.strip().replace("\n", " ")[:120],
117
+ url=base + path,
118
+ )
119
+ )
120
+ except Exception:
121
+ continue
122
+ return findings