headerhawk-auditor 0.1.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.
headerhawk/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """HeaderHawk: read-only HTTP security configuration checks."""
2
+ __version__ = "0.1.0"
headerhawk/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
headerhawk/audit.py ADDED
@@ -0,0 +1,56 @@
1
+ """Pure response analysis; no networking in this module."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import asdict, dataclass
5
+ from typing import Any
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class Finding:
10
+ id: str
11
+ severity: str
12
+ title: str
13
+ recommendation: str
14
+ passed: bool
15
+
16
+ def to_dict(self) -> dict[str, Any]:
17
+ return asdict(self)
18
+
19
+
20
+ def audit_response(url: str, headers: dict[str, str], cookies: list[str] | None = None) -> dict[str, Any]:
21
+ h = {k.lower(): v for k, v in headers.items()}
22
+ findings: list[Finding] = []
23
+
24
+ def check(id: str, severity: str, title: str, ok: bool, recommendation: str) -> None:
25
+ findings.append(Finding(id, severity, title, recommendation, ok))
26
+
27
+ https = url.lower().startswith("https://")
28
+ check("https", "high", "HTTPS in use", https, "Serve the site over HTTPS.")
29
+ check("hsts", "medium", "HTTP Strict Transport Security", not https or bool(h.get("strict-transport-security")), "Set Strict-Transport-Security on HTTPS responses after validating rollout readiness.")
30
+ check("csp", "medium", "Content Security Policy", bool(h.get("content-security-policy")), "Deploy a restrictive Content-Security-Policy appropriate for the application.")
31
+ check("nosniff", "medium", "MIME sniffing protection", h.get("x-content-type-options", "").lower().strip() == "nosniff", "Set X-Content-Type-Options: nosniff.")
32
+ check("referrer", "low", "Referrer policy", bool(h.get("referrer-policy")), "Set a privacy-conscious Referrer-Policy such as strict-origin-when-cross-origin.")
33
+ check("permissions", "low", "Permissions policy", bool(h.get("permissions-policy")), "Define Permissions-Policy to limit unused browser features.")
34
+ check("frame", "medium", "Framing protection", bool(h.get("x-frame-options")) or "frame-ancestors" in h.get("content-security-policy", "").lower(), "Use CSP frame-ancestors (or X-Frame-Options for older browsers).")
35
+ check("cache", "info", "Cache-control header", bool(h.get("cache-control")), "Review Cache-Control based on whether the response contains sensitive data.")
36
+
37
+ for i, raw in enumerate(cookies or [], start=1):
38
+ # Cookie values are never included in findings or reports.
39
+ # Attribute matching is case-insensitive and works with nonstandard cookie values.
40
+ attributes = raw.split(";", 1)[1].lower() if ";" in raw else ""
41
+ parts = [part.strip() for part in attributes.split(";")]
42
+ has_secure = any(part == "secure" for part in parts)
43
+ has_httponly = any(part == "httponly" for part in parts)
44
+ has_samesite = any(part.startswith("samesite=") for part in parts)
45
+ prefix = f"cookie-{i}"
46
+ check(f"{prefix}-secure", "medium", f"Cookie {i}: Secure flag", has_secure, "Set Secure for cookies sent over HTTPS.")
47
+ check(f"{prefix}-httponly", "medium", f"Cookie {i}: HttpOnly flag", has_httponly, "Set HttpOnly for cookies not needed by JavaScript.")
48
+ check(f"{prefix}-samesite", "low", f"Cookie {i}: SameSite attribute", has_samesite, "Set SameSite=Lax or Strict where appropriate.")
49
+
50
+ # Score is a configuration heuristic, not a security guarantee.
51
+ weighted = {"high": 15, "medium": 10, "low": 5, "info": 0}
52
+ scored = [f for f in findings if weighted[f.severity] > 0 and not (f.id == "hsts" and not https)]
53
+ possible = sum(weighted[f.severity] for f in scored)
54
+ earned = sum(weighted[f.severity] for f in scored if f.passed)
55
+ score = round(100 * earned / possible) if possible else 100
56
+ return {"url": url, "score": score, "passed": sum(f.passed for f in findings), "total": len(findings), "findings": [f.to_dict() for f in findings]}
headerhawk/cli.py ADDED
@@ -0,0 +1,105 @@
1
+ """Command line entrypoint for HeaderHawk."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import html
6
+ import ipaddress
7
+ import json
8
+ import socket
9
+ import sys
10
+ from pathlib import Path
11
+ from urllib.parse import urlsplit
12
+
13
+ import httpx
14
+
15
+ from . import __version__
16
+ from .audit import audit_response
17
+
18
+
19
+ def validate_url(url: str, allow_private: bool = False) -> None:
20
+ parts = urlsplit(url)
21
+ if parts.scheme not in {"http", "https"} or not parts.hostname or parts.username or parts.password:
22
+ raise ValueError("Supply an http(s) URL without embedded credentials.")
23
+ if not allow_private:
24
+ try:
25
+ addresses = socket.getaddrinfo(parts.hostname, parts.port or (443 if parts.scheme == "https" else 80), type=socket.SOCK_STREAM)
26
+ except socket.gaierror as exc:
27
+ raise ValueError(f"DNS resolution failed: {exc}") from exc
28
+ if not addresses or any(not ipaddress.ip_address(entry[4][0]).is_global for entry in addresses):
29
+ raise ValueError("Private, loopback, link-local, and non-global destinations are blocked by default. Use --allow-private for trusted local targets.")
30
+
31
+
32
+ def fetch(url: str, timeout: float, allow_private: bool) -> dict:
33
+ validate_url(url, allow_private)
34
+ # No redirects: prevents silently following to an unexpected destination.
35
+ # No proxy environment variables: avoids unexpected routing.
36
+ with httpx.Client(
37
+ timeout=timeout,
38
+ follow_redirects=False,
39
+ trust_env=False,
40
+ headers={"User-Agent": f"HeaderHawk/{__version__}"},
41
+ ) as client, client.stream("GET", url) as response:
42
+ headers = dict(response.headers)
43
+ cookies = response.headers.get_list("set-cookie")
44
+ result = audit_response(str(response.url), headers, cookies)
45
+ result["status_code"] = response.status_code
46
+ result["redirect_location_present"] = "location" in response.headers
47
+ return result
48
+
49
+
50
+ def render_html(result: dict) -> str:
51
+ rows = "\n".join(
52
+ f"<tr><td>{html.escape(item['title'])}</td><td>{html.escape(item['severity'])}</td><td>{'PASS' if item['passed'] else 'REVIEW'}</td><td>{html.escape(item['recommendation'])}</td></tr>"
53
+ for item in result["findings"]
54
+ )
55
+ return ("<!doctype html><html lang='en'><head><meta charset='utf-8'><title>HeaderHawk report</title>"
56
+ "<style>body{font:16px system-ui;max-width:1100px;margin:3rem auto;padding:0 1rem;color:#17212d}table{border-collapse:collapse;width:100%}td,th{border:1px solid #ccc;padding:.6rem;text-align:left}tr:nth-child(even){background:#f4f6f8}</style></head><body>"
57
+ f"<h1>HeaderHawk report</h1><p>URL: {html.escape(result['url'])}</p><p>Configuration score: {result['score']}/100</p>"
58
+ f"<p>HTTP status: {result.get('status_code', 'N/A')}</p><table><thead><tr><th>Check</th><th>Severity</th><th>Result</th><th>Recommendation</th></tr></thead><tbody>{rows}</tbody></table>"
59
+ "<p>Heuristic checks only; a passing score does not establish application security.</p></body></html>")
60
+
61
+
62
+ def main(argv: list[str] | None = None) -> int:
63
+ parser = argparse.ArgumentParser(prog="headerhawk", description="Read-only HTTP security header and cookie auditor")
64
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
65
+ sub = parser.add_subparsers(dest="command", required=True)
66
+ scan = sub.add_parser("scan", help="Audit one HTTP(S) URL")
67
+ scan.add_argument("url")
68
+ scan.add_argument("--format", choices=["text", "json", "html"], default="text")
69
+ scan.add_argument("--output", type=Path, help="Write report to a file")
70
+ scan.add_argument("--timeout", type=float, default=10.0)
71
+ scan.add_argument("--allow-private", action="store_true", help="Permit localhost and private network targets")
72
+ args = parser.parse_args(argv)
73
+ if args.timeout <= 0 or args.timeout > 60:
74
+ parser.error("--timeout must be greater than 0 and at most 60")
75
+ try:
76
+ result = fetch(args.url, args.timeout, args.allow_private)
77
+ except (ValueError, httpx.HTTPError, OSError) as exc:
78
+ print(f"headerhawk: {exc}", file=sys.stderr)
79
+ return 2
80
+ if args.format == "json":
81
+ content = json.dumps(result, indent=2)
82
+ elif args.format == "html":
83
+ content = render_html(result)
84
+ else:
85
+ lines = [f"HeaderHawk | {result['url']}", f"HTTP {result['status_code']} | Configuration score: {result['score']}/100"]
86
+ for item in result["findings"]:
87
+ lines.append(f"[{'PASS' if item['passed'] else 'REVIEW'}] {item['title']} ({item['severity']})")
88
+ if not item["passed"]:
89
+ lines.append(f" {item['recommendation']}")
90
+ lines.append("Note: configuration heuristic only; not a vulnerability assessment.")
91
+ content = "\n".join(lines)
92
+ if args.output:
93
+ try:
94
+ args.output.write_text(content + "\n", encoding="utf-8")
95
+ except OSError as exc:
96
+ print(f"headerhawk: cannot write report: {exc}", file=sys.stderr)
97
+ return 2
98
+ print(f"Report saved to {args.output}")
99
+ else:
100
+ print(content)
101
+ return 0
102
+
103
+
104
+ if __name__ == "__main__":
105
+ raise SystemExit(main())
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.5
2
+ Name: headerhawk-auditor
3
+ Version: 0.1.0
4
+ Summary: A lightweight, read-only HTTP security header and cookie auditor
5
+ Author: Noir Stack LLC
6
+ License: Proprietary Software License — Free Use, No Modification
7
+ License-File: LICENSE
8
+ Keywords: audit,cli,headers,http,security
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Security
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: httpx<1,>=0.27
13
+ Provides-Extra: dev
14
+ Requires-Dist: build>=1.2; extra == 'dev'
15
+ Requires-Dist: pytest>=8; extra == 'dev'
16
+ Requires-Dist: ruff>=0.6; extra == 'dev'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # HeaderHawk
20
+
21
+ A small, read-only HTTP security-header and cookie configuration auditor, designed as a portfolio-ready Python package.
22
+
23
+ > HeaderHawk checks response configuration. It is **not** a penetration test, does not prove exploitability, and its score is not a security certification.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ python -m pip install -e '.[dev]'
29
+ ```
30
+
31
+ Once published to PyPI, install using `pip install headerhawk-auditor` (subject to package name availability).
32
+
33
+ ## Usage
34
+
35
+ ```bash
36
+ headerhawk scan https://example.com
37
+ headerhawk scan https://example.com --format json --output report.json
38
+ headerhawk scan https://example.com --format html --output report.html
39
+ python -m headerhawk scan https://example.com
40
+ ```
41
+
42
+ Checks: HTTPS, HSTS, Content-Security-Policy, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, framing protection, Cache-Control presence, and Secure/HttpOnly/SameSite cookie attributes. Header checks are intentionally conservative: for example, a CSP being present does not mean it is effective. Cookies may legitimately need different flags based on application purpose. Cache-Control is informational.
43
+
44
+ Reports do not include cookie values or redirect destinations. When a response is a redirect, HeaderHawk records only that a `Location` header was present.
45
+
46
+ No redirects are followed; HTTP redirects must be audited by explicitly scanning the destination URL. No response body is downloaded beyond response headers. Private and loopback destinations are blocked unless `--allow-private` is provided. The DNS check is a best-effort guard, not a complete defense against DNS rebinding; do not run this tool as a privileged or public-facing URL-fetching service.
47
+
48
+ Only audit websites you own or have permission to assess.
49
+
50
+ ## Development
51
+
52
+ ```bash
53
+ python -m pip install -e '.[dev]'
54
+ pytest -q
55
+ ruff check .
56
+ python -m build
57
+ ```
58
+
59
+ ## Publishing
60
+
61
+ 1. Choose a unique PyPI distribution name and update `pyproject.toml`.
62
+ 2. Create a GitHub repository and configure PyPI Trusted Publishing for the repository's `publish.yml` workflow and `pypi` environment.
63
+ 3. Tag a release such as `v0.1.0`, then run the publish workflow after verifying the build.
64
+ 4. Never put API tokens or credentials in repository files.
65
+
66
+ ## Limitations
67
+
68
+ This MVP audits one response at a time. It does not crawl, brute-force, exploit, verify TLS certificate chains independently, or evaluate the semantics of every header directive. Findings are heuristic and can contain false positives or negatives.
69
+
70
+ ## License
71
+
72
+ Copyright © 2026 Noir Stack LLC. Licensed under the
73
+ [Proprietary Software License — Free Use, No Modification](LICENSE).
@@ -0,0 +1,9 @@
1
+ headerhawk/__init__.py,sha256=elk9rcK-Ld4KFpBofaPFBEPFOsAXQzlxrfEBf2rlRUI,86
2
+ headerhawk/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
3
+ headerhawk/audit.py,sha256=kPx2rNnQkVimPFkyKr--K2F4YAmDv6TGVbGHSTT-lXY,3652
4
+ headerhawk/cli.py,sha256=vpAOARUimaf1MBGHocGGTK25tWMA4AftqBPp5lVlktg,5193
5
+ headerhawk_auditor-0.1.0.dist-info/METADATA,sha256=oBBo-1sDGP3MAmywOC2KdWWW7ufJmxG87eEHNZRv_NA,3233
6
+ headerhawk_auditor-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
7
+ headerhawk_auditor-0.1.0.dist-info/entry_points.txt,sha256=hacrXIQ5aQlsZnypTym-uqh_TKyGtleFk_Dz9lMa4wg,51
8
+ headerhawk_auditor-0.1.0.dist-info/licenses/LICENSE,sha256=WMqIJXD3rBuirXChYblF7ipgFsKgYZR2DdUpEV-agjk,1527
9
+ headerhawk_auditor-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ headerhawk = headerhawk.cli:main
@@ -0,0 +1,38 @@
1
+ Proprietary Software License — Free Use, No Modification
2
+
3
+ Copyright (c) 2026 Noir Stack LLC. All rights reserved.
4
+
5
+ ## 1. Grant of License
6
+
7
+ The copyright holder grants users a non-exclusive, worldwide, royalty-free license to download, install, and use this software for personal, educational, research, and commercial purposes, subject to the conditions below.
8
+
9
+ ## 2. Restrictions
10
+
11
+ Users may not, without prior written permission from the copyright holder:
12
+
13
+ - Modify, adapt, translate, or create derivative works based on the software.
14
+ - Redistribute, publish, sell, sublicense, or repackage the software.
15
+ - Remove or alter copyright, attribution, or license notices.
16
+ - Claim ownership or authorship of the software.
17
+
18
+ ## 3. Ownership
19
+
20
+ All rights, title, and interest in the software, including its source code and associated intellectual property, remain with the copyright holder.
21
+
22
+ No ownership rights are transferred under this license.
23
+
24
+ ## 4. No Warranty
25
+
26
+ The software is provided "AS IS", without warranty of any kind, express or implied, to the extent permitted by applicable law.
27
+
28
+ ## 5. Limitation of Liability
29
+
30
+ To the extent permitted by applicable law, the copyright holder shall not be liable for damages arising from the use or inability to use the software.
31
+
32
+ ## 6. Termination
33
+
34
+ Permission to use the software terminates upon a material violation of this license, subject to applicable law.
35
+
36
+ ## 7. Additional Permissions
37
+
38
+ Any permissions not expressly granted must be obtained in writing from the copyright holder.