diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..fbba9358 --- /dev/null +++ b/TODO.md @@ -0,0 +1,48 @@ +# SeedPass TODO + +This file tracks the remaining work from the latest bug search and security evaluation. + +## Security + +- [ ] Upgrade vulnerable runtime dependencies in lockfiles and packaging inputs: + - `aiohttp` -> `>=3.13.3` + - `cryptography` -> `>=46.0.5` + - `starlette` -> `>=0.49.1` + - `python-multipart` -> `>=0.0.22` + - `urllib3` -> `>=2.6.3` + - `pynacl` -> `>=1.6.2` + - `pillow` -> `>=12.1.1` + - `orjson`, `pyasn1`, `cbor2` and other `pip-audit` findings +- [ ] Regenerate `requirements.lock` after upgrades and re-run the full test suite. +- [ ] Re-run `pip-audit -r requirements.lock` and ensure zero actionable runtime vulnerabilities. +- [ ] Evaluate the `Crypto.*` imports in `src/seedpass/core/password_generation.py` and either: + - keep `pycryptodome` intentionally with explicit documentation, or + - migrate RSA generation to `cryptography` APIs. + +## Robustness + +- [ ] Replace broad `except Exception: pass` blocks with explicit exceptions + logging: + - `src/main.py` + - `src/seedpass/core/manager.py` + - `src/seedpass/core/menu_handler.py` +- [ ] Add telemetry-safe logs for swallowed error paths without exposing secrets. + +## Tests To Add + +- [ ] Add deterministic tests for `derive_pgp_key(..., key_type="rsa")` covering: + - stable output across repeated runs + - key fingerprint format and validity +- [ ] Add API rate-limit tests for: + - per-client isolation + - window expiry behavior + - restart/reset behavior after `start_server()` +- [ ] Add API import tests for boundary conditions: + - exactly-at-limit upload size + - malformed multipart payload + - invalid JSON body for path-based import +- [ ] Add regression tests for manager notification/logging fallback paths currently guarded by broad exception handling. + +## Documentation + +- [ ] Document API import upload limit (`SEEDPASS_MAX_IMPORT_BYTES`) in user docs. +- [ ] Add a security maintenance section describing dependency-audit cadence and update policy. diff --git a/landing/index.html b/landing/index.html index c96bbb1f..d558c130 100644 --- a/landing/index.html +++ b/landing/index.html @@ -76,6 +76,16 @@ flowchart TB classDef default fill:#ffffff,stroke:#e94a39,stroke-width:2px,color:#283c4f; Get Started +
+

Install SeedPass (stable)

+
+
bash -c "$(curl -sSL https://raw.githubusercontent.com/PR0M3TH3AN/SeedPass/main/scripts/install.sh)"
+ +
+
@@ -257,4 +267,4 @@ Enter your choice (1-7) or press Enter to exit: - \ No newline at end of file + diff --git a/landing/script.js b/landing/script.js index c8202fe7..5d714604 100644 --- a/landing/script.js +++ b/landing/script.js @@ -65,5 +65,34 @@ document.querySelectorAll('a[href^="#"]').forEach(anchor => { // Log when the page is loaded document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('[data-copy-target]').forEach((button) => { + button.addEventListener('click', async () => { + const targetId = button.getAttribute('data-copy-target'); + const target = document.getElementById(targetId); + if (!target) return; + + const text = target.textContent.trim(); + + try { + await navigator.clipboard.writeText(text); + } catch (_err) { + const temp = document.createElement('textarea'); + temp.value = text; + document.body.appendChild(temp); + temp.select(); + document.execCommand('copy'); + document.body.removeChild(temp); + } + + const original = button.innerHTML; + button.classList.add('copied'); + button.innerHTML = ' Copied'; + setTimeout(() => { + button.classList.remove('copied'); + button.innerHTML = original; + }, 1800); + }); + }); + console.log('SeedPass landing page loaded'); }); diff --git a/landing/style.css b/landing/style.css index 7061f5b0..e6606a7c 100644 --- a/landing/style.css +++ b/landing/style.css @@ -231,6 +231,14 @@ footer .social-media a:focus { .nav-links.active { display: flex; } + + .install-command-shell { + flex-direction: column; + } + + .install-copy-button { + min-height: 40px; + } } /* Section Titles */ @@ -337,6 +345,53 @@ footer .social-media a:focus { transform: translateY(-3px); } +.install-quickstart { + margin: 28px auto 0; + max-width: 900px; + text-align: left; +} + +.install-label { + margin: 0 0 8px; + font-size: 0.95rem; + color: rgba(255, 255, 255, 0.9); +} + +.install-command-shell { + display: flex; + align-items: stretch; + gap: 10px; +} + +.install-code { + flex: 1; + margin: 0; +} + +.install-copy-button { + border: 1px solid rgba(255, 255, 255, 0.25); + background: rgba(255, 255, 255, 0.08); + color: #ffffff; + border-radius: 8px; + padding: 0 14px; + font-weight: 600; + cursor: pointer; + white-space: nowrap; + transition: background-color 0.2s ease, border-color 0.2s ease; +} + +.install-copy-button:hover, +.install-copy-button:focus { + background: rgba(255, 255, 255, 0.18); + border-color: rgba(255, 255, 255, 0.45); + outline: none; +} + +.install-copy-button.copied { + background: #2ea043; + border-color: #2ea043; +} + /* Mini flow chart in hero */ .mini-chart { max-width: 600px; @@ -985,6 +1040,20 @@ body.dark-mode .code-block { box-shadow: 0 2px 8px var(--shadow-dark); } +body.dark-mode .install-label { + color: #c9d1d9; +} + +body.dark-mode .install-copy-button { + border-color: rgba(255, 255, 255, 0.3); + background: rgba(255, 255, 255, 0.08); +} + +body.dark-mode .install-copy-button:hover, +body.dark-mode .install-copy-button:focus { + background: rgba(255, 255, 255, 0.18); +} + body.dark-mode .disclaimer .container { background-color: #2c2f33; color: var(--footer-text); @@ -1002,4 +1071,4 @@ body.dark-mode .disclaimer ul li:focus-within { body.dark-mode footer { background-color: var(--background-dark); -} \ No newline at end of file +} diff --git a/src/seedpass/api.py b/src/seedpass/api.py index 82c25dac..c9dd7e9b 100644 --- a/src/seedpass/api.py +++ b/src/seedpass/api.py @@ -10,6 +10,7 @@ import queue import time from collections import defaultdict, deque from typing import Any, List, Optional +import hashlib import logging @@ -48,7 +49,8 @@ async def dynamic_cors_headers(request: Request, call_next): def _get_pm(request: Request) -> PasswordManager: pm = getattr(request.app.state, "pm", None) - assert pm is not None + if pm is None: + raise HTTPException(status_code=503, detail="Server not initialized") return pm @@ -66,7 +68,8 @@ def _check_token(request: Request, auth: str | None) -> None: def _enforce_rate_limit(request: Request, raw_token: str) -> None: now = time.monotonic() client = request.client.host if request.client else "unknown" - key = f"{client}:{raw_token}" + token_tag = hashlib.blake2s(raw_token.encode("utf-8"), digest_size=8).hexdigest() + key = f"{client}:{token_tag}" buckets = getattr(request.app.state, "rate_limit_buckets", None) if buckets is None: buckets = defaultdict(deque) diff --git a/src/tests/test_api.py b/src/tests/test_api.py index 64a85cbf..e85bc1a9 100644 --- a/src/tests/test_api.py +++ b/src/tests/test_api.py @@ -9,6 +9,21 @@ sys.path.append(str(Path(__file__).resolve().parents[1])) from seedpass import api +def test_get_pm_requires_initialized_state(): + class DummyState: + pass + + class DummyApp: + state = DummyState() + + class DummyRequest: + app = DummyApp() + + with pytest.raises(api.HTTPException) as exc: + api._get_pm(DummyRequest()) + assert exc.value.status_code == 503 + + @pytest.mark.anyio async def test_token_hashed(client): _, token = client diff --git a/src/tests/test_api_rate_limit.py b/src/tests/test_api_rate_limit.py index 8f41d9e6..c3531ae9 100644 --- a/src/tests/test_api_rate_limit.py +++ b/src/tests/test_api_rate_limit.py @@ -45,3 +45,6 @@ async def test_rate_limit_exceeded(monkeypatch): res = await client.get("/api/v1/entry", params={"query": "s"}, headers=headers) assert res.status_code == 429 + bucket_keys = list(api.app.state.rate_limit_buckets.keys()) + assert bucket_keys + assert all(token not in key for key in bucket_keys)