hackable 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.
hackable/report.py ADDED
@@ -0,0 +1,244 @@
1
+ """Reports: terminal, JSON, and self-contained HTML."""
2
+
3
+ import html
4
+ import json
5
+ from datetime import datetime, timezone
6
+
7
+ from . import __version__
8
+ from .findings import SEVERITIES, score_findings
9
+
10
+ RESET = "\033[0m"
11
+ BOLD = "\033[1m"
12
+ DIM = "\033[2m"
13
+
14
+ SEV_COLOR = {
15
+ "critical": "\033[1;31m",
16
+ "high": "\033[31m",
17
+ "medium": "\033[33m",
18
+ "low": "\033[34m",
19
+ "info": "\033[90m",
20
+ }
21
+
22
+ SEV_ICON = {
23
+ "critical": "\u2716", # ✖
24
+ "high": "\u2716",
25
+ "medium": "!",
26
+ "low": "i",
27
+ "info": "\u2139",
28
+ }
29
+
30
+ VERDICTS = {
31
+ "A": "Solid. Scan again after every deploy to stay here.",
32
+ "B": "Good shape. Fix the mediums when you get an afternoon.",
33
+ "C": "Shaky. The highs above are worth fixing this week.",
34
+ "D": "Risky. An attacker would enjoy this site.",
35
+ "F": "A beginner hacker could break in within minutes. Fix the criticals today.",
36
+ }
37
+
38
+ SEV_CSS = {
39
+ "critical": "#ff5555",
40
+ "high": "#ff8c42",
41
+ "medium": "#f1c40f",
42
+ "low": "#5aa9ff",
43
+ "info": "#8a8f98",
44
+ }
45
+
46
+
47
+ def _c(text, code, use_color):
48
+ return "%s%s%s" % (code, text, RESET) if use_color else text
49
+
50
+
51
+ def render_terminal(findings, base, elapsed, req_count, use_color=True):
52
+ score, grade = score_findings(findings)
53
+ L = []
54
+ c = lambda t, code: _c(t, code, use_color) # noqa: E731
55
+
56
+ L.append("")
57
+ L.append(c("hackable v" + __version__, BOLD) + c(" - hack yourself before they do.", DIM))
58
+ L.append("")
59
+ L.append("Target: %s (%d requests in %.1fs)" % (base, req_count, elapsed))
60
+ L.append("")
61
+
62
+ grouped = {s: [f for f in findings if f.severity == s] for s in SEVERITIES}
63
+ if not findings:
64
+ L.append(c("No issues found. Either your site is solid, or it hid well.", BOLD))
65
+ for sev in SEVERITIES:
66
+ items = grouped[sev]
67
+ if not items:
68
+ continue
69
+ L.append(c("%s (%d)" % (sev.upper(), len(items)), SEV_COLOR[sev] + BOLD))
70
+ for f in items:
71
+ L.append(
72
+ " %s %s"
73
+ % (c(SEV_ICON[sev], SEV_COLOR[sev]), c(f.title, BOLD))
74
+ )
75
+ L.append(" What this means: %s" % f.meaning)
76
+ L.append(" Fix: %s" % f.fix)
77
+ if f.url:
78
+ L.append(c(" Found at: %s" % f.url, DIM))
79
+ if f.evidence:
80
+ ev = f.evidence.replace("\n", " ")
81
+ L.append(c(" Evidence: %s%s" % (ev[:140], "..." if len(ev) > 140 else ""), DIM))
82
+ L.append("")
83
+
84
+ L.append(c("-" * 60, DIM))
85
+ L.append(
86
+ "SCORE %s GRADE %s"
87
+ % (c("%d/100" % score, BOLD), c(grade, SEV_COLOR["critical"] if grade in "DF" else BOLD))
88
+ )
89
+ L.append(VERDICTS[grade])
90
+ L.append("")
91
+ return "\n".join(L)
92
+
93
+
94
+ def render_json(findings, base, elapsed, req_count):
95
+ score, grade = score_findings(findings)
96
+ return json.dumps(
97
+ {
98
+ "tool": "hackable " + __version__,
99
+ "target": base,
100
+ "scanned_at": datetime.now(timezone.utc).isoformat(),
101
+ "requests": req_count,
102
+ "elapsed_s": round(elapsed, 1),
103
+ "score": score,
104
+ "grade": grade,
105
+ "findings": [
106
+ {
107
+ "check": f.check,
108
+ "severity": f.severity,
109
+ "title": f.title,
110
+ "meaning": f.meaning,
111
+ "fix": f.fix,
112
+ "evidence": f.evidence,
113
+ "url": f.url,
114
+ }
115
+ for f in findings
116
+ ],
117
+ },
118
+ indent=2,
119
+ )
120
+
121
+
122
+ SARIF_LEVELS = {
123
+ "critical": "error",
124
+ "high": "error",
125
+ "medium": "warning",
126
+ "low": "note",
127
+ "info": "note",
128
+ }
129
+
130
+
131
+ def render_sarif(findings, base, elapsed, req_count):
132
+ """SARIF 2.1.0 output, consumable by GitHub code scanning."""
133
+ rules, seen = [], set()
134
+ for f in findings:
135
+ if f.check not in seen:
136
+ seen.add(f.check)
137
+ rules.append({
138
+ "id": "hackable/" + f.check,
139
+ "name": f.check.replace("_", " ").title(),
140
+ "shortDescription": {"text": f.title},
141
+ "helpUri": "https://github.com/Shifu34/hackable",
142
+ })
143
+ results = []
144
+ for f in findings:
145
+ results.append({
146
+ "ruleId": "hackable/" + f.check,
147
+ "level": SARIF_LEVELS.get(f.severity, "note"),
148
+ "message": {"text": "%s. %s" % (f.title, f.fix or "")},
149
+ "locations": [{
150
+ "physicalLocation": {
151
+ "artifactLocation": {"uri": f.url or base},
152
+ }
153
+ }],
154
+ })
155
+ return json.dumps(
156
+ {
157
+ "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
158
+ "version": "2.1.0",
159
+ "runs": [{
160
+ "tool": {"driver": {
161
+ "name": "hackable",
162
+ "version": __version__,
163
+ "informationUri": "https://github.com/Shifu34/hackable",
164
+ "rules": rules,
165
+ }},
166
+ "results": results,
167
+ }],
168
+ },
169
+ indent=2,
170
+ )
171
+
172
+ def render_html(findings, base, elapsed, req_count):
173
+ score, grade = score_findings(findings)
174
+ esc = html.escape
175
+ cards = []
176
+ for f in findings:
177
+ color = SEV_CSS[f.severity]
178
+ cards.append(
179
+ """
180
+ <div class="card">
181
+ <div class="card-head" style="border-left-color: {color}">
182
+ <span class="sev" style="background: {color}">{sev}</span>
183
+ <span class="title">{title}</span>
184
+ </div>
185
+ <p><b>What this means:</b> {meaning}</p>
186
+ <p><b>How to fix it:</b> {fix}</p>
187
+ {url}
188
+ {evidence}
189
+ </div>""".format(
190
+ color=color,
191
+ sev=f.severity.upper(),
192
+ title=esc(f.title),
193
+ meaning=esc(f.meaning),
194
+ fix=esc(f.fix),
195
+ url='<p class="meta">Found at: <code>%s</code></p>' % esc(f.url) if f.url else "",
196
+ evidence='<p class="meta">Evidence: <code>%s</code></p>' % esc(f.evidence[:200])
197
+ if f.evidence
198
+ else "",
199
+ )
200
+ )
201
+ if not cards:
202
+ cards.append('<div class="card"><p><b>No issues found.</b></p></div>')
203
+ return """<!DOCTYPE html>
204
+ <html lang="en"><head><meta charset="utf-8">
205
+ <title>hackable report: {base}</title>
206
+ <style>
207
+ body {{ font-family: -apple-system, Segoe UI, Roboto, sans-serif; background: #0d1117;
208
+ color: #e6edf3; max-width: 860px; margin: 0 auto; padding: 32px 20px; }}
209
+ h1 {{ font-size: 22px; }} .dim {{ color: #8a8f98; }}
210
+ .scorebox {{ display: flex; align-items: center; gap: 24px; background: #161b22;
211
+ border: 1px solid #30363d; border-radius: 12px; padding: 20px 24px; margin: 20px 0; }}
212
+ .score {{ font-size: 56px; font-weight: 800; }}
213
+ .grade {{ font-size: 40px; font-weight: 800; width: 72px; height: 72px; border-radius: 50%;
214
+ display: flex; align-items: center; justify-content: center; border: 3px solid; }}
215
+ .card {{ background: #161b22; border: 1px solid #30363d; border-radius: 12px;
216
+ padding: 16px 20px; margin: 14px 0; }}
217
+ .card-head {{ border-left: 5px solid; padding-left: 12px; margin-bottom: 8px;
218
+ display: flex; align-items: center; gap: 10px; }}
219
+ .sev {{ color: #0d1117; font-weight: 700; font-size: 12px; padding: 3px 8px; border-radius: 6px; }}
220
+ .title {{ font-weight: 700; font-size: 16px; }}
221
+ .meta {{ color: #8a8f98; font-size: 13px; }} code {{ font-size: 12.5px; }}
222
+ .footer {{ margin-top: 28px; color: #8a8f98; font-size: 13px; }}
223
+ </style></head><body>
224
+ <h1>hackable report</h1>
225
+ <p class="dim">Target: {base} &middot; {nreq} requests in {elapsed:.1f}s &middot; {date}</p>
226
+ <div class="scorebox">
227
+ <div class="score">{score}<span style="font-size:22px;color:#8a8f98">/100</span></div>
228
+ <div class="grade" style="border-color:{gcolor};color:{gcolor}">{grade}</div>
229
+ <div><b>{verdict}</b></div>
230
+ </div>
231
+ {cards}
232
+ <p class="footer">Generated by hackable " + __version__ + " &mdash; hack yourself before they do.
233
+ Only scan apps you own or have permission to test.</p>
234
+ </body></html>""".format(
235
+ base=esc(base),
236
+ nreq=req_count,
237
+ elapsed=elapsed,
238
+ date=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"),
239
+ score=score,
240
+ grade=grade,
241
+ gcolor="#ff5555" if grade in "DF" else "#f1c40f" if grade == "C" else "#3fb950",
242
+ verdict=esc(VERDICTS[grade]),
243
+ cards="\n".join(cards),
244
+ )
hackable/scanner.py ADDED
@@ -0,0 +1,60 @@
1
+ """Scan orchestration."""
2
+
3
+ import time
4
+
5
+ from .checks import CHECKS
6
+ from .crawler import discover
7
+ from .polite import PoliteClient
8
+
9
+ # checks that probe the crawler's discovered inputs
10
+ INJECTION_CHECKS = ("sqli", "xss", "open_redirect")
11
+
12
+
13
+ def normalize_target(target, http):
14
+ target = target.strip().rstrip("/")
15
+ if "://" not in target:
16
+ for scheme in ("https://", "http://"):
17
+ r = http.get(scheme + target + "/")
18
+ if r is not None and r.status_code < 500:
19
+ return scheme + target
20
+ return "http://" + target
21
+ return target
22
+
23
+
24
+ def scan(target, http=None, on_check=None, include=None, exclude=None):
25
+ """Run checks. Returns (base_url, findings, elapsed_s, requests_made).
26
+
27
+ include/exclude are optional sets of check ids (--only / --skip).
28
+ """
29
+ http = http or PoliteClient()
30
+ base = normalize_target(target, http)
31
+ findings = []
32
+ started = time.time()
33
+
34
+ wanted = [c for c in CHECKS
35
+ if (include is None or c[0] in include)
36
+ and (exclude is None or c[0] not in exclude)]
37
+
38
+ discovered = []
39
+ if any(cid in INJECTION_CHECKS for cid, _l, _f in wanted):
40
+ if on_check:
41
+ on_check("mapping the app")
42
+ try:
43
+ discovered = discover(base, http)
44
+ except Exception:
45
+ discovered = []
46
+
47
+ for cid, label, fn in wanted:
48
+ if on_check:
49
+ on_check(label)
50
+ try:
51
+ if cid in INJECTION_CHECKS:
52
+ findings.extend(fn(base, http, discovered) or [])
53
+ else:
54
+ findings.extend(fn(base, http) or [])
55
+ except RuntimeError:
56
+ break # request budget exceeded: stop gracefully
57
+ except Exception:
58
+ continue # one broken check must never kill the scan
59
+ elapsed = time.time() - started
60
+ return base, findings, elapsed, http.count
@@ -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,29 @@
1
+ hackable/__init__.py,sha256=dZjbcpwg8kXe8j8-QOzByBECRIGLst5KKhUNAjDLgno,69
2
+ hackable/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
3
+ hackable/cli.py,sha256=DJMID9N2jVYWF7BgdlxZ0PLuWgnD6C9gREKe8n6enR4,4368
4
+ hackable/crawler.py,sha256=Y-SnvIr_GAeVK36_WL_UjicTMtIjxrb0wxo8BNRBoGg,3174
5
+ hackable/findings.py,sha256=GgIwafofuTzezOgwNVOAlHuD98d0E882kmRFLvmjWtk,975
6
+ hackable/polite.py,sha256=AqRK5-46HlN0ZgfH2inShs7BdWrmxem_5EtqJ8oGwGo,1483
7
+ hackable/report.py,sha256=5mv-mT0SWi2vkTtjdf-g0ktvuQamttUhS20FwX0XuMc,8414
8
+ hackable/scanner.py,sha256=RpV50JkV1WEgJta6psbs8NjvTga4VTrcxXR9-tmOLlE,1875
9
+ hackable/checks/__init__.py,sha256=7KeJ_28kasDva6d6EVYrbcjp9qFl5_j5VuJj4ojID98,1095
10
+ hackable/checks/cookies.py,sha256=23aRpp7uLQxP51OgqdUG7NST82K_PPAA7cvS149SsjM,2464
11
+ hackable/checks/cors.py,sha256=XKLTC14_YuicsSDDsaJJ6BSmTNfTG1G0afyG4PH0D5g,2648
12
+ hackable/checks/debug.py,sha256=uUiVnmVp0gEJzeF8NwcWA7jnSrDY_YDMTwM-rHezFIE,1266
13
+ hackable/checks/disclosure.py,sha256=PwvrEsQDGw5a1-glZxG-BYgSfmKTifHHL6GuG8JGBGI,1129
14
+ hackable/checks/exposed_files.py,sha256=Xd5szLa9_TEA3tjw1s_P_0srhXBjw7y5L-YKiqzIUMY,3933
15
+ hackable/checks/headers.py,sha256=xtqFUHQlYQqSURNyRThd2dwCQ75cIG2sJ_VZGwDgg9I,2452
16
+ hackable/checks/methods.py,sha256=gJcKZN_rGBDq5BD_StpdPBhcSYszaDndsNlgONjxHSs,970
17
+ hackable/checks/open_redirect.py,sha256=DN0GhbPSw8mrnuB6VDrIX_sES87CDDKad-bJu7CK4xI,1908
18
+ hackable/checks/rate_limit.py,sha256=HmESBgfPNi0f1mdH_YBbxneKEVF_ccBM-1ySACjr2oE,1864
19
+ hackable/checks/robots.py,sha256=ZgJTS3hR6DTQPfyOP5oC8aik8fry6kOe7UyfywhWpwg,1068
20
+ hackable/checks/securitytxt.py,sha256=_1MmH7giAx7LqieXnKjMgjDK9gdoQqja4IiOI5ycuV0,903
21
+ hackable/checks/sqli.py,sha256=1yggTDA_o00QtKKYERumDE_KAr7VSd7kCEorwBF6vZ4,5839
22
+ hackable/checks/tls.py,sha256=97zjljxwA7ejj-oZb7JLJwi8VmwVMEeuldnU8wIUCyY,3492
23
+ hackable/checks/xss.py,sha256=V3I1o_HhRPC3zCuVdqzkJKYSS8KhwyVZcL-qBi5qLgA,2868
24
+ hackable-1.0.0.dist-info/licenses/LICENSE,sha256=hPiqvko3E-3DQj4evYl0HLpJAgaXnwIsTSDOB0_PQz8,1072
25
+ hackable-1.0.0.dist-info/METADATA,sha256=y286pySg-C_oTAwojPRjRAJlNV9fDAc2FBmSV0Ot95Y,4009
26
+ hackable-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
27
+ hackable-1.0.0.dist-info/entry_points.txt,sha256=3PoYi5B1CfebEKG9oGn0Wpd5493b8ewDsMy6brfM3r4,47
28
+ hackable-1.0.0.dist-info/top_level.txt,sha256=xl05JWEasdJ0My8hhPg-WnE_Kwn0BnczLA5LhHbBl24,9
29
+ hackable-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ hackable = hackable.cli:main
@@ -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 @@
1
+ hackable