From eac9eea8d057746d987550db219314e13cf35cc5 Mon Sep 17 00:00:00 2001 From: akvnn Date: Mon, 6 Oct 2025 23:32:33 +0530 Subject: [PATCH 1/3] backend --- api/app.py | 116 +- api/smoke_domain.py | 18 + frontend/package-lock.json | 1551 ++++++++++++++++- frontend/package.json | 4 +- frontend/src/App.jsx | 225 ++- frontend/src/components/FX.jsx | 481 +++++ frontend/src/styles.css | 78 +- frontend/vite.config.js | 1 + package.json | 9 + .../domain_infrastructure/dns_enumeration.py | 1 + .../domain_infrastructure/port_scanning.py | 9 +- .../subdomain_discovery.py | 38 +- .../domain_infrastructure/whois_lookup.py | 5 +- redcalibur/osint/shodan_integration.py | 20 +- redcalibur/osint/url_health_check.py | 31 + .../osint/user_identity/username_lookup.py | 89 +- redcalibur/osint/virustotal_integration.py | 28 +- tests/conftest.py | 8 + tests/test_basic_functionality.py | 6 +- 19 files changed, 2477 insertions(+), 241 deletions(-) create mode 100644 api/smoke_domain.py create mode 100644 frontend/src/components/FX.jsx create mode 100644 package.json create mode 100644 redcalibur/osint/url_health_check.py create mode 100644 tests/conftest.py diff --git a/api/app.py b/api/app.py index d43bea8..9e26a6d 100644 --- a/api/app.py +++ b/api/app.py @@ -10,9 +10,12 @@ from redcalibur.osint.domain_infrastructure.subdomain_discovery import discover_subdomains from redcalibur.osint.domain_infrastructure.port_scanning import perform_port_scan from redcalibur.osint.domain_infrastructure.ssl_tls_details import get_ssl_details +from concurrent.futures import ThreadPoolExecutor, as_completed, TimeoutError +import time from redcalibur.osint.network_threat_intel.shodan_integration import perform_shodan_scan from redcalibur.osint.user_identity.username_lookup import lookup_username from redcalibur.osint.virustotal_integration import scan_url +from redcalibur.osint.url_health_check import basic_url_health from redcalibur.osint.ai_enhanced.recon_summarizer import summarize_recon_data from redcalibur.osint.ai_enhanced.risk_scoring import calculate_risk_score @@ -65,44 +68,77 @@ def domain_recon(req: DomainRequest): results: Dict[str, Any] = {"target": req.target, "timestamp": datetime.now().isoformat()} errors: Dict[str, Any] = {} - # WHOIS - if req.whois or req.all: - try: - results["whois"] = perform_whois_lookup(req.target) - except Exception as e: - logger.error(f"WHOIS error: {e}") - errors["whois"] = str(e) - - # DNS - if req.dns or req.all: - try: - results["dns"] = enumerate_dns_records(req.target) - except Exception as e: - logger.error(f"DNS error: {e}") - errors["dns"] = str(e) + tasks = [] - # Subdomains - if req.subdomains or req.all: + def wrap(name, fn, *args, **kwargs): try: - results["subdomains"] = discover_subdomains(req.target, config.SUBDOMAIN_WORDLIST) + res = fn(*args, **kwargs) + results[name] = res except Exception as e: - logger.error(f"Subdomain discovery error: {e}") - errors["subdomains"] = str(e) + logger.error(f"{name} error: {e}") + errors[name] = str(e) - # SSL - if req.ssl or req.all: - try: - results["ssl"] = get_ssl_details(req.target) - except Exception as e: - logger.error(f"SSL details error: {e}") - errors["ssl"] = str(e) + ex = ThreadPoolExecutor(max_workers=4) + try: + fut_to_name: Dict[Any, str] = {} + if req.whois or req.all: + fut = ex.submit(wrap, "whois", perform_whois_lookup, req.target) + tasks.append(fut) + fut_to_name[fut] = "whois" + if req.dns or req.all: + fut = ex.submit(wrap, "dns", enumerate_dns_records, req.target) + tasks.append(fut) + fut_to_name[fut] = "dns" + if req.subdomains or req.all: + fut = ex.submit(wrap, "subdomains", discover_subdomains, req.target, config.SUBDOMAIN_WORDLIST) + tasks.append(fut) + fut_to_name[fut] = "subdomains" + if req.ssl or req.all: + fut = ex.submit(wrap, "ssl", get_ssl_details, req.target) + tasks.append(fut) + fut_to_name[fut] = "ssl" + + deadline = time.time() + 12.0 + pending = set(tasks) + while time.time() < deadline and pending: + try: + for fut in as_completed(list(pending), timeout=0.5): + try: + fut.result() + except Exception as e: + logger.error(f"Task future error: {e}") + finally: + pending.discard(fut) + except TimeoutError: + # No futures completed in this slice; loop again until deadline + pass + + # Mark any remaining as timed out by task name + for fut in list(pending): + name = fut_to_name.get(fut, "task") + errors[name] = "timeout" + finally: + # Do not wait for running tasks; return partial results promptly + ex.shutdown(wait=False, cancel_futures=True) # AI enrichment (optional) if req.all: try: import json raw_data = json.dumps(results, indent=2, default=str) - results["ai_summary"] = summarize_recon_data(raw_data[:2000]) + # Run summarization with a strict timeout to avoid long external calls + ex_ai = ThreadPoolExecutor(max_workers=1) + try: + fut = ex_ai.submit(summarize_recon_data, raw_data[:2000]) + try: + results["ai_summary"] = fut.result(timeout=6.0) + except TimeoutError: + errors["ai"] = "timeout" + finally: + # Don't wait for the AI call to finish if it's slow + ex_ai.shutdown(wait=False, cancel_futures=True) + + # Risk scoring is local and fast features = [ len(results.get("subdomains", [])), 1 if isinstance(results.get("ssl", {}), dict) and "error" not in results.get("ssl", {}) else 0, @@ -152,17 +188,23 @@ def username(req: UsernameRequest): @app.post("/urlscan") def urlscan(req: URLScanRequest): try: - if not config.VIRUSTOTAL_API_KEY: - raise HTTPException(status_code=400, detail="VIRUSTOTAL_API_KEY not configured") - data = scan_url(config.VIRUSTOTAL_API_KEY, req.url) - if not data: - raise HTTPException(status_code=502, detail="VirusTotal returned no data") - return data - except HTTPException: - raise + # Run the scan in a short-lived thread with a strict timeout + def runner(): + if not config.VIRUSTOTAL_API_KEY: + return {"note": "VIRUSTOTAL_API_KEY not configured", "health": basic_url_health(req.url)} + return scan_url(config.VIRUSTOTAL_API_KEY, req.url) or {"error": "no_data"} + + with ThreadPoolExecutor(max_workers=1) as ex_url: + fut = ex_url.submit(runner) + try: + return fut.result(timeout=10.0) + except TimeoutError: + return {"error": "timeout"} + finally: + ex_url.shutdown(wait=False, cancel_futures=True) except Exception as e: logger.error(f"URL scan failed: {e}") - raise HTTPException(status_code=500, detail=str(e)) + return {"error": str(e)} class SummarizeRequest(BaseModel): diff --git a/api/smoke_domain.py b/api/smoke_domain.py new file mode 100644 index 0000000..c82c74d --- /dev/null +++ b/api/smoke_domain.py @@ -0,0 +1,18 @@ +import json +import requests + +def main(): + url = "http://127.0.0.1:8000/domain" + payload = {"target": "example.com", "all": True} + try: + r = requests.post(url, json=payload, timeout=20) + print("Status:", r.status_code) + try: + print(json.dumps(r.json(), indent=2)) + except Exception: + print(r.text) + except Exception as e: + print("Request failed:", e) + +if __name__ == "__main__": + main() diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 417a88f..c779b2d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,7 +9,9 @@ "version": "0.1.0", "dependencies": { "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-markdown": "^9.0.1", + "remark-gfm": "^4.0.0" }, "devDependencies": { "@types/react": "^18.2.48", @@ -1184,25 +1186,64 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.25", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.25.tgz", "integrity": "sha512-oSVZmGtDPmRZtVDqvdKUi/qgCsWp5IDY29wp8na8Bj4B3cc99hfNzvNhlMkVVxctkAOGUA3Km7MMpBHAnWfcIA==", - "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -1219,6 +1260,18 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1332,6 +1385,16 @@ "postcss": "^8.1.0" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1450,6 +1513,56 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1508,6 +1621,16 @@ "dev": true, "license": "MIT" }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -1557,14 +1680,12 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1578,6 +1699,41 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -1662,6 +1818,34 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -1828,6 +2012,86 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1857,6 +2121,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1890,6 +2164,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -1900,6 +2184,18 @@ "node": ">=0.12.0" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1965,58 +2261,911 @@ "node": ">=6" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT" }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", "dependencies": { - "yallist": "^3.0.2" + "micromark-util-types": "^2.0.0" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "engines": { - "node": ">= 8" + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -2061,7 +3210,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mz": { @@ -2149,6 +3297,31 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2393,6 +3566,16 @@ "dev": true, "license": "MIT" }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2439,6 +3622,33 @@ "react": "^18.3.1" } }, + "node_modules/react-markdown": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", + "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -2472,6 +3682,72 @@ "node": ">=8.10.0" } }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -2635,6 +3911,16 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -2699,6 +3985,20 @@ "node": ">=8" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", @@ -2739,6 +4039,24 @@ "node": ">=8" } }, + "node_modules/style-to-js": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz", + "integrity": "sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.9" + } + }, + "node_modules/style-to-object": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz", + "integrity": "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -2849,6 +4167,26 @@ "node": ">=8.0" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -2856,6 +4194,93 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -2894,6 +4319,34 @@ "dev": true, "license": "MIT" }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "5.4.20", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", @@ -3074,6 +4527,16 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 7c30db1..02ed15c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,9 @@ }, "dependencies": { "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-markdown": "^9.0.1", + "remark-gfm": "^4.0.0" }, "devDependencies": { "@vitejs/plugin-react": "^4.3.2", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index ce0c654..ba519d5 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,33 +1,64 @@ import React, { useEffect, useMemo, useState } from 'react' +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import VisualFX from './components/FX' + const API_BASE = import.meta.env.VITE_API_BASE || '/api' const Section = ({ title, children }) => (
-

{title}

+

{title}

{children}
) -const Header = ({ apiStatus }) => ( -
-
- - - - -
-
RedCalibur
-
Red Teaming AI — Recon, Scan, Report
- - - API - +const Header = ({ palette, setPalette, fx, setFx, apiStatus }) => { + const [openFx, setOpenFx] = useState(false) + return ( +
+
+ + + + +
+
RedCalibur
+
Red Teaming AI — Recon, Scan, Report
+
+
+
+ API: {apiStatus} +
+ +
+ + {openFx && ( +
+
Effects
+
+ {Object.keys(fx).map(key => ( + + ))} +
+
+ +
+
+ )} +
+
-
-
-) + + ) +} export default function App() { const [loading, setLoading] = useState(false) @@ -35,115 +66,128 @@ export default function App() { const [logs, setLogs] = useState([]) const [apiStatus, setApiStatus] = useState('unknown') - const api = useMemo(() => ({ - post: async (path, body, { updateResult = true } = {}) => { + const defaultFx = { matrix:true,constellation:true,ripple:true,mouseTrail:true,noise:true,scanlines:true,sweep:true,vignette:true,parallax:true } + const [palette, setPalette] = useState(() => (typeof window==='undefined' ? 'red' : (localStorage.getItem('rc_palette')||'red'))) + const [fx, setFx] = useState(() => { + if (typeof window==='undefined') return defaultFx + try { + const saved = JSON.parse(localStorage.getItem('rc_fx')||'null') + return saved && typeof saved==='object' ? { ...defaultFx, ...saved } : defaultFx + } catch { return defaultFx } + }) + const [showSummary, setShowSummary] = useState(false) + const [summaryText, setSummaryText] = useState('') + const [summaryLoading, setSummaryLoading] = useState(false) + + useEffect(() => { + const root = document.documentElement + if (palette==='blue') { + root.style.setProperty('--primary-rgb','59,130,246') + root.style.setProperty('--secondary-rgb','244,63,94') + } else { + root.style.setProperty('--primary-rgb','244,63,94') + root.style.setProperty('--secondary-rgb','59,130,246') + } + try { localStorage.setItem('rc_palette', palette) } catch {} + }, [palette]) + + useEffect(() => { try { localStorage.setItem('rc_fx', JSON.stringify(fx)) } catch {} }, [fx]) + + const api = useMemo(()=>({ + post: async (path, body, {updateResult=true}={}) => { setLoading(true) const start = performance.now() try { - const res = await fetch(`${API_BASE}${path}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body) - }) - const json = await res.json() - if (!res.ok) throw new Error(json?.detail || 'Request failed') + const res = await fetch(`${API_BASE}${path}`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) }) + const json = await res.json(); if (!res.ok) throw new Error(json?.detail||'Request failed') if (updateResult) setResult(json) - setLogs(l => [{ time: new Date().toLocaleTimeString(), path, ok: true, ms: Math.round(performance.now() - start) }, ...l]) + setLogs(l=>[{ time:new Date().toLocaleTimeString(), path, ok:true, ms:Math.round(performance.now()-start) }, ...l]) return json - } catch (e) { - setLogs(l => [{ time: new Date().toLocaleTimeString(), path, ok: false, error: e.message }, ...l]) + } catch(e) { + setLogs(l=>[{ time:new Date().toLocaleTimeString(), path, ok:false, error:e.message }, ...l]) throw e - } finally { - setLoading(false) - } + } finally { setLoading(false) } } }), []) - // API health probe - useEffect(() => { - let mounted = true - const ping = async () => { - try { - const res = await fetch(`${API_BASE}/health`) - if (!mounted) return - setApiStatus(res.ok ? 'ok' : 'down') - } catch { - if (!mounted) return - setApiStatus('down') - } - } - ping() - const id = setInterval(ping, 10000) - return () => { mounted = false; clearInterval(id) } - }, []) + useEffect(()=>{ + let mounted=true + const ping=async()=>{ try{ const res=await fetch(`${API_BASE}/health`); if(!mounted) return; setApiStatus(res.ok?'ok':'down') } catch { if(!mounted) return; setApiStatus('down') } } + ping(); const id=setInterval(ping,10000); return ()=>{mounted=false; clearInterval(id)} + },[]) return (
-
-
+ +
+
-
- api.post('/domain', data)} loading={loading} /> -
-
- api.post('/scan', data)} loading={loading} /> -
-
- api.post('/username', data)} loading={loading} /> -
-
- api.post('/urlscan', data)} loading={loading} /> -
+
api.post('/domain', data)} loading={loading} />
+
api.post('/scan', data)} loading={loading} />
+
api.post('/username', data)} loading={loading} />
+
api.post('/urlscan', data)} loading={loading} />
-
{result ? : } {result && (
- +
)}
-
- -
+
+ {showSummary && ( +
+
setShowSummary(false)} /> +
+
+

AI Summary

+
+ + +
+
+
+ {summaryLoading ? 'Summarizing…' : (summaryText ? ( +

, + h2:({node,...props})=>

, + h3:({node,...props})=>

, + p: ({node,...props})=>

, + ul:({node,...props})=>

+
+
+ )}
) } function Field({ label, children }) { return ( -
) } + diff --git a/frontend/src/components/FX.jsx b/frontend/src/components/FX.jsx new file mode 100644 index 0000000..9fac26c --- /dev/null +++ b/frontend/src/components/FX.jsx @@ -0,0 +1,481 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react' + +// helper to build rgba from an "r,g,b" base string +function rgba(base, a) { + return `rgba(${base},${a})` +} + +export function CursorGlow({ color = 'rgba(244,63,94,0.35)' }) { + const ref = useRef(null) + useEffect(() => { + const el = ref.current + if (!el) return + let raf + const onMove = (e) => { + const x = e.clientX + const y = e.clientY + // slight easing for smoothness + cancelAnimationFrame(raf) + raf = requestAnimationFrame(() => { + el.style.transform = `translate(${x - 150}px, ${y - 150}px)` + }) + } + window.addEventListener('mousemove', onMove, { passive: true }) + return () => { + cancelAnimationFrame(raf) + window.removeEventListener('mousemove', onMove) + } + }, []) + return ( +
+ ) +} + +// Draw a smooth line trail that follows the mouse and fades over time +export function MouseTrail({ enabled = true, colorBase = '244,63,94', glowBase = '59,130,246' }) { + const canvasRef = useRef(null) + const reduced = usePrefersReducedMotionBool() + useEffect(() => { + if (!enabled || reduced) return + const canvas = canvasRef.current + if (!canvas) return + const ctx = canvas.getContext('2d') + let width = (canvas.width = window.innerWidth) + let height = (canvas.height = window.innerHeight) + + // trail points with timestamp + const points = [] + const MAX_AGE = 700 // ms + const SMOOTHING = 0.15 // interpolation for the virtual cursor + const MAX_POINTS = 120 // cap for performance + let vx = width / 2, + vy = height / 2 + + const onResize = () => { + width = canvas.width = window.innerWidth + height = canvas.height = window.innerHeight + } + const onMove = (e) => { + // ease a virtual cursor towards the actual cursor for smoothness + const tx = e.clientX + const ty = e.clientY + vx += (tx - vx) * SMOOTHING + vy += (ty - vy) * SMOOTHING + points.push({ x: vx, y: vy, t: performance.now() }) + if (points.length > MAX_POINTS) points.shift() + } + window.addEventListener('resize', onResize) + window.addEventListener('mousemove', onMove, { passive: true }) + + let raf + const draw = () => { + const now = performance.now() + // fade previous strokes by reducing alpha, without painting any color + ctx.save() + ctx.globalCompositeOperation = 'destination-out' + ctx.fillStyle = 'rgba(0,0,0,0.06)' + ctx.fillRect(0, 0, width, height) + ctx.restore() + + // remove old points + for (let i = points.length - 1; i >= 0; i--) { + if (now - points[i].t > MAX_AGE) points.splice(i, 1) + } + + if (points.length > 1) { + // draw a smoothed polyline using quadratic curves + ctx.lineCap = 'round' + ctx.lineJoin = 'round' + + for (let i = 1; i < points.length; i++) { + const p0 = points[i - 1] + const p1 = points[i] + const age = now - p1.t + const life = 1 - Math.min(age / MAX_AGE, 1) + const alpha = life * 0.9 + const widthScale = 1 + life * 3 // thicker when fresh + ctx.strokeStyle = rgba(colorBase, alpha) + ctx.lineWidth = 1.2 * widthScale + ctx.beginPath() + // control point halfway for a smooth curve + const cx = (p0.x + p1.x) / 2 + const cy = (p0.y + p1.y) / 2 + ctx.moveTo(p0.x, p0.y) + ctx.quadraticCurveTo(cx, cy, p1.x, p1.y) + ctx.stroke() + } + + // subtle outer glow pass using additive blend + ctx.save() + ctx.globalCompositeOperation = 'lighter' + for (let i = 1; i < points.length; i++) { + const p0 = points[i - 1] + const p1 = points[i] + const age = now - p1.t + const life = 1 - Math.min(age / MAX_AGE, 1) + const alpha = life * 0.25 + ctx.strokeStyle = rgba(glowBase, alpha) + ctx.lineWidth = 2.5 + ctx.beginPath() + const cx = (p0.x + p1.x) / 2 + const cy = (p0.y + p1.y) / 2 + ctx.moveTo(p0.x, p0.y) + ctx.quadraticCurveTo(cx, cy, p1.x, p1.y) + ctx.stroke() + } + ctx.restore() + } + + raf = requestAnimationFrame(draw) + } + draw() + + return () => { + cancelAnimationFrame(raf) + window.removeEventListener('resize', onResize) + window.removeEventListener('mousemove', onMove) + } + }, [enabled]) + + return ( + + ) +} + +export function Scanlines() { + return ( +
+ ) +} + +export function Noise() { + // CSS-animated noise using background-position jitter + return ( +
+ ) +} + +function usePrefersReducedMotion() { + return useMemo(() => + typeof window !== 'undefined' && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches + , []) +} + +export function MatrixRain({ enabled = true }) { + const canvasRef = useRef(null) + const reduced = usePrefersReducedMotion() + const colorBase = arguments[0]?.colorBase || '244,63,94' + + useEffect(() => { + if (!enabled || reduced) return + const canvas = canvasRef.current + if (!canvas) return + const ctx = canvas.getContext('2d') + let width = canvas.width = window.innerWidth + let height = canvas.height = window.innerHeight + let animationFrame + const fontSize = 14 + const columns = Math.floor(width / fontSize) + const drops = new Array(columns).fill(1) + + const onResize = () => { + width = canvas.width = window.innerWidth + height = canvas.height = window.innerHeight + } + window.addEventListener('resize', onResize) + + const draw = () => { + // translucent background for trail effect + ctx.fillStyle = 'rgba(0,0,0,0.08)' + ctx.fillRect(0, 0, width, height) + ctx.fillStyle = rgba(colorBase, 0.65) + ctx.font = `${fontSize}px monospace` + for (let i = 0; i < drops.length; i++) { + const text = String.fromCharCode(0x30A0 + Math.random() * 96) + const x = i * fontSize + const y = drops[i] * fontSize + ctx.fillText(text, x, y) + if (y > height && Math.random() > 0.975) drops[i] = 0 + drops[i]++ + } + animationFrame = requestAnimationFrame(draw) + } + draw() + + return () => { + cancelAnimationFrame(animationFrame) + window.removeEventListener('resize', onResize) + } + }, [enabled, reduced]) + + return ( + + ) +} + +function usePrefersReducedMotionBool() { + if (typeof window === 'undefined' || !window.matchMedia) return false + return window.matchMedia('(prefers-reduced-motion: reduce)').matches +} + +export function Constellation({ enabled = true, colorBase = '244,63,94' }) { + const canvasRef = useRef(null) + const reduced = usePrefersReducedMotionBool() + useEffect(() => { + if (!enabled || reduced) return + const canvas = canvasRef.current + if (!canvas) return + const ctx = canvas.getContext('2d') + let width = canvas.width = window.innerWidth + let height = canvas.height = window.innerHeight + const particles = [] + const count = Math.min(120, Math.floor((width * height) / 18000)) + const mouse = { x: width / 2, y: height / 2 } + for (let i = 0; i < count; i++) { + particles.push({ + x: Math.random() * width, + y: Math.random() * height, + vx: (Math.random() - 0.5) * 0.3, + vy: (Math.random() - 0.5) * 0.3 + }) + } + const onMove = (e) => { mouse.x = e.clientX; mouse.y = e.clientY } + const onResize = () => { width = canvas.width = window.innerWidth; height = canvas.height = window.innerHeight } + window.addEventListener('mousemove', onMove, { passive: true }) + window.addEventListener('resize', onResize) + let raf + const draw = () => { + ctx.clearRect(0, 0, width, height) + // draw lines + for (let i = 0; i < particles.length; i++) { + const p = particles[i] + // slight attraction to mouse + const dx = mouse.x - p.x + const dy = mouse.y - p.y + p.vx += (dx / 50000) + p.vy += (dy / 50000) + p.x += p.vx + p.y += p.vy + if (p.x < 0 || p.x > width) p.vx *= -1 + if (p.y < 0 || p.y > height) p.vy *= -1 + } + ctx.lineWidth = 1 + for (let i = 0; i < particles.length; i++) { + for (let j = i + 1; j < particles.length; j++) { + const a = particles[i], b = particles[j] + const dx = a.x - b.x, dy = a.y - b.y + const dist2 = dx * dx + dy * dy + const maxDist2 = 90 * 90 + if (dist2 < maxDist2) { + const alpha = 1 - dist2 / maxDist2 + ctx.strokeStyle = rgba(colorBase, alpha * 0.35) + ctx.beginPath() + ctx.moveTo(a.x, a.y) + ctx.lineTo(b.x, b.y) + ctx.stroke() + } + } + } + // draw points + for (let i = 0; i < particles.length; i++) { + const p = particles[i] + ctx.fillStyle = rgba(colorBase, 0.7) + ctx.fillRect(p.x, p.y, 2, 2) + } + raf = requestAnimationFrame(draw) + } + draw() + return () => { cancelAnimationFrame(raf); window.removeEventListener('mousemove', onMove); window.removeEventListener('resize', onResize) } + }, [enabled]) + return +} + +export function ParallaxLayer() { + const ref = useRef(null) + useEffect(() => { + const el = ref.current + if (!el) return + let raf + const onMove = (e) => { + const x = (e.clientX / window.innerWidth - 0.5) * 8 + const y = (e.clientY / window.innerHeight - 0.5) * 8 + cancelAnimationFrame(raf) + raf = requestAnimationFrame(() => { + el.style.transform = `translate3d(${x}px, ${y}px, 0)` + }) + } + window.addEventListener('mousemove', onMove, { passive: true }) + return () => { cancelAnimationFrame(raf); window.removeEventListener('mousemove', onMove) } + }, []) + return ( +
+ ) +} + +export function SweepGlow() { + return ( +
+ ) +} + +export function Vignette() { + return ( +
+ ) +} + +export function RippleFX({ enabled = true }) { + const canvasRef = useRef(null) + const reduced = usePrefersReducedMotionBool() + useEffect(() => { + if (!enabled || reduced) return + const canvas = canvasRef.current + if (!canvas) return + const ctx = canvas.getContext('2d') + let width = canvas.width = window.innerWidth + let height = canvas.height = window.innerHeight + const ripples = [] + const MAX_AGE = 1200 // ms + const SPEED = 0.6 // pixels per ms + + const onResize = () => { + width = canvas.width = window.innerWidth + height = canvas.height = window.innerHeight + } + const onClick = (e) => { + ripples.push({ x: e.clientX, y: e.clientY, t: performance.now() }) + } + window.addEventListener('resize', onResize) + window.addEventListener('mousedown', onClick) + + let raf + const draw = () => { + const now = performance.now() + ctx.clearRect(0, 0, width, height) + ctx.globalCompositeOperation = 'lighter' + for (let i = ripples.length - 1; i >= 0; i--) { + const r = ripples[i] + const age = now - r.t + if (age > MAX_AGE) { ripples.splice(i, 1); continue } + const prog = age / MAX_AGE + const radius = age * SPEED + const alpha = (1 - prog) * 0.35 + + // outer soft glow + const grad = ctx.createRadialGradient(r.x, r.y, Math.max(radius - 40, 0), r.x, r.y, radius + 60) + grad.addColorStop(0, `rgba(244,63,94,${alpha * 0.35})`) + grad.addColorStop(0.5, `rgba(59,130,246,${alpha * 0.25})`) + grad.addColorStop(1, 'rgba(0,0,0,0)') + ctx.fillStyle = grad + ctx.beginPath() + ctx.arc(r.x, r.y, radius + 60, 0, Math.PI * 2) + ctx.fill() + + // ring ripples + ctx.lineWidth = 2 + const rings = 3 + for (let k = 0; k < rings; k++) { + const ringR = radius - k * 24 + if (ringR <= 0) continue + const a = Math.max(alpha - k * 0.08, 0) + ctx.strokeStyle = `rgba(244,63,94,${a})` + ctx.beginPath() + ctx.arc(r.x, r.y, ringR, 0, Math.PI * 2) + ctx.stroke() + } + } + ctx.globalCompositeOperation = 'source-over' + raf = requestAnimationFrame(draw) + } + draw() + + return () => { + cancelAnimationFrame(raf) + window.removeEventListener('resize', onResize) + window.removeEventListener('mousedown', onClick) + } + }, [enabled]) + return ( + + ) +} + +export default function VisualFX({ + palette = 'red', + flags = {}, + matrix = true, + noise = true, + scanlines = true, +}) { + // color bases are simple "r,g,b" strings to combine with varying alpha + const colors = palette === 'blue' + ? { primary: '59,130,246', secondary: '244,63,94' } + : { primary: '244,63,94', secondary: '59,130,246' } + const [enableMatrix, setEnableMatrix] = useState(() => typeof window !== 'undefined' ? window.innerWidth >= 768 : true) + useEffect(() => { + const onResize = () => setEnableMatrix(window.innerWidth >= 768) + window.addEventListener('resize', onResize) + return () => window.removeEventListener('resize', onResize) + }, []) + return ( + <> + {/* Base layers (furthest back) */} + {flags.parallax !== false && } + {flags.vignette !== false && } + {flags.sweep !== false && } + {(flags.matrix !== false && matrix) && } + {flags.constellation !== false && } + {flags.ripple !== false && } + {flags.mouseTrail !== false && ( + + )} + {(flags.noise !== false && noise) && } + {(flags.scanlines !== false && scanlines) && } + + ) +} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 09a4198..ff583a7 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -2,9 +2,18 @@ @tailwind components; @tailwind utilities; +:root { + /* default palette = red primary, blue secondary */ + --primary-rgb: 244,63,94; /* red */ + --secondary-rgb: 59,130,246;/* blue */ +} + body { - background-image: radial-gradient(circle at 1px 1px, rgba(244,63,94,0.2) 1px, transparent 0); - background-size: 24px 24px; + background-image: + radial-gradient(circle at 1px 1px, rgba(var(--primary-rgb),0.18) 1px, transparent 0), + radial-gradient(1200px 800px at 80% -10%, rgba(var(--primary-rgb),0.08), transparent 60%), + radial-gradient(800px 600px at 10% 110%, rgba(var(--secondary-rgb),0.06), transparent 60%); + background-size: 24px 24px, auto, auto; } .card { @@ -18,3 +27,68 @@ body { .input { @apply w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-gray-100 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-redcalibur-600; } + +/* glitch title effect */ +.glitch { + position: relative; + text-shadow: 0 0 12px rgba(244,63,94,0.5); +} +.glitch::before, +.glitch::after { + content: attr(data-text); + position: absolute; + top: 0; left: 0; + width: 100%; height: 100%; + overflow: hidden; +} +.glitch::before { + left: 1px; text-shadow: -1px 0 rgba(59,130,246,0.6); + animation: glitch-shift 3s infinite linear alternate-reverse; +} +.glitch::after { + left: -1px; text-shadow: 1px 0 rgba(244,63,94,0.6); + animation: glitch-shift 2.2s infinite linear alternate; +} +@keyframes glitch-shift { + 0% { clip-path: inset(0 0 85% 0); } + 20% { clip-path: inset(0 0 20% 0); } + 40% { clip-path: inset(60% 0 0 0); } + 60% { clip-path: inset(0 0 65% 0); } + 80% { clip-path: inset(30% 0 0 0); } + 100% { clip-path: inset(0 0 0 0); } +} + +/* animated noise overlay */ +.noise-overlay { + background-image: url('data:image/svg+xml;utf8,'); + animation: noise-move 1.5s steps(10) infinite; +} +@keyframes noise-move { from { background-position: 0 0; } to { background-position: 100% 100%; } } + +/* sweeping glow accent */ +.sweep-glow { + position: fixed; + inset: -20% -10% -20% -10%; + pointer-events: none; + z-index: 0; + background: conic-gradient( + from 0deg at 50% 50%, + transparent 0deg, + rgba(var(--primary-rgb),0.08) 90deg, + transparent 180deg, + rgba(var(--secondary-rgb),0.06) 270deg, + transparent 360deg + ); + animation: sweep 14s linear infinite; + filter: blur(40px); +} +@keyframes sweep { to { transform: rotate(360deg); } } + +/* soft vignette to focus center */ +.vignette { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 0; + background: radial-gradient(circle at 50% 50%, transparent 60%, rgba(0,0,0,0.6) 100%); +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 610e72e..faca8c7 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -5,6 +5,7 @@ export default defineConfig({ plugins: [react()], server: { port: 5173, + host: '::', proxy: { '/api': { target: 'http://127.0.0.1:8000', diff --git a/package.json b/package.json new file mode 100644 index 0000000..e9b657b --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "redcalibur-monorepo", + "private": true, + "scripts": { + "dev": "npm run dev --prefix frontend", + "build": "npm run build --prefix frontend", + "preview": "npm run preview --prefix frontend" + } +} diff --git a/redcalibur/osint/domain_infrastructure/dns_enumeration.py b/redcalibur/osint/domain_infrastructure/dns_enumeration.py index c5d96f0..24f0204 100755 --- a/redcalibur/osint/domain_infrastructure/dns_enumeration.py +++ b/redcalibur/osint/domain_infrastructure/dns_enumeration.py @@ -4,6 +4,7 @@ def _resolver(nameservers=None, timeout=5.0): r = dns.resolver.Resolver(configure=True) + r.timeout = float(timeout) r.lifetime = float(timeout) if nameservers: r.nameservers = nameservers diff --git a/redcalibur/osint/domain_infrastructure/port_scanning.py b/redcalibur/osint/domain_infrastructure/port_scanning.py index 5786c11..c17eff1 100755 --- a/redcalibur/osint/domain_infrastructure/port_scanning.py +++ b/redcalibur/osint/domain_infrastructure/port_scanning.py @@ -15,8 +15,11 @@ def perform_port_scan(target, ports): for port in ports: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.settimeout(1) - result = s.connect_ex((target, port)) - port_status[port] = "Open" if result == 0 else "Closed" + s.settimeout(0.6) + try: + result = s.connect_ex((target, port)) + port_status[port] = "Open" if result == 0 else "Closed" + except Exception as e: + port_status[port] = f"Error: {e}" return port_status diff --git a/redcalibur/osint/domain_infrastructure/subdomain_discovery.py b/redcalibur/osint/domain_infrastructure/subdomain_discovery.py index c986172..a319403 100755 --- a/redcalibur/osint/domain_infrastructure/subdomain_discovery.py +++ b/redcalibur/osint/domain_infrastructure/subdomain_discovery.py @@ -1,25 +1,45 @@ import requests +from concurrent.futures import ThreadPoolExecutor, as_completed -def discover_subdomains(domain, subdomain_list): +def discover_subdomains(domain, subdomain_list, timeout: float = 2.0, workers: int = 8): """ Discover subdomains for the given domain using a wordlist. Args: domain (str): The domain name to search. subdomain_list (list): A list of potential subdomains. + timeout (float): Per-request timeout seconds. + workers (int): Number of parallel workers. Returns: - list: A list of discovered subdomains. + list: A list of discovered subdomains (URLs). """ discovered_subdomains = [] - for subdomain in subdomain_list: - url = f"http://{subdomain}.{domain}" + def probe(sub: str): + url = f"http://{sub}.{domain}" try: - response = requests.get(url) - if response.status_code == 200: - discovered_subdomains.append(url) - except requests.ConnectionError: - pass + # HEAD is lighter; some hosts may not support it, so fallback to GET + resp = requests.head(url, timeout=timeout, allow_redirects=True) + if resp.status_code < 400: + return url + # Fallback to GET if HEAD inconclusive + resp = requests.get(url, timeout=timeout, allow_redirects=True) + if resp.status_code < 400: + return url + except requests.RequestException: + return None + return None + + with ThreadPoolExecutor(max_workers=max(1, workers)) as ex: + futures = {ex.submit(probe, sub): sub for sub in subdomain_list} + for fut in as_completed(futures): + try: + res = fut.result() + if res: + discovered_subdomains.append(res) + except Exception: + # ignore individual probe failures + pass return discovered_subdomains diff --git a/redcalibur/osint/domain_infrastructure/whois_lookup.py b/redcalibur/osint/domain_infrastructure/whois_lookup.py index e93f168..e2576b7 100755 --- a/redcalibur/osint/domain_infrastructure/whois_lookup.py +++ b/redcalibur/osint/domain_infrastructure/whois_lookup.py @@ -2,7 +2,7 @@ import whois import requests -def perform_whois_lookup(domain): +def perform_whois_lookup(domain, timeout: float = 6.0): """ Perform a WHOIS lookup for the given domain. @@ -13,6 +13,7 @@ def perform_whois_lookup(domain): dict: A dictionary containing WHOIS information. """ try: + # whois module doesn't support timeout directly; rely on its internal defaults whois_info = whois.whois(domain) if isinstance(whois_info, dict): return whois_info @@ -20,7 +21,7 @@ def perform_whois_lookup(domain): except Exception as e: # Fallback to RDAP if whois fails try: - resp = requests.get(f"https://rdap.org/domain/{domain}", timeout=8) + resp = requests.get(f"https://rdap.org/domain/{domain}", timeout=max(2.0, timeout)) if resp.ok: data = resp.json() return {"rdap": data, "whois_error": str(e)} diff --git a/redcalibur/osint/shodan_integration.py b/redcalibur/osint/shodan_integration.py index 55d89c9..782a0b0 100755 --- a/redcalibur/osint/shodan_integration.py +++ b/redcalibur/osint/shodan_integration.py @@ -12,10 +12,14 @@ def search_shodan(api_key: str, query: str): try: results = api.search(query) - print(f"Results found: {results['total']}") - for result in results['matches']: - print(f"IP: {result['ip_str']}") - print(result['data']) + total = results.get('total', 0) if isinstance(results, dict) else 0 + print(f"Results found: {total}") + for result in results.get('matches', []) if isinstance(results, dict) else []: + ip = result.get('ip_str', 'unknown') + print(f"IP: {ip}") + banner = result.get('data') + if banner: + print(banner) print("") return results @@ -35,13 +39,13 @@ def get_host_info(api_key: str, ip: str): try: host = api.host(ip) - print(f"IP: {host['ip_str']}") + print(f"IP: {host.get('ip_str', ip)}") print(f"Organization: {host.get('org', 'n/a')}") print(f"Operating System: {host.get('os', 'n/a')}") - for item in host['data']: - print(f"Port: {item['port']}") - print(f"Banner: {item['data']}") + for item in host.get('data', []) if isinstance(host, dict) else []: + print(f"Port: {item.get('port', 'n/a')}") + print(f"Banner: {item.get('data', '')}") return host except shodan.APIError as e: diff --git a/redcalibur/osint/url_health_check.py b/redcalibur/osint/url_health_check.py new file mode 100644 index 0000000..afaa80a --- /dev/null +++ b/redcalibur/osint/url_health_check.py @@ -0,0 +1,31 @@ +import requests +from urllib.parse import urlparse + +DEFAULT_TIMEOUT = 6.0 + + +def normalize_url(url: str) -> str: + if not url.lower().startswith(("http://", "https://")): + return "http://" + url + return url + + +def basic_url_health(url: str) -> dict: + """ + Lightweight URL health check used when VirusTotal is unavailable. + Returns status code, final URL, and a small set of headers. + """ + out = {"input": url} + try: + url = normalize_url(url) + resp = requests.get(url, timeout=DEFAULT_TIMEOUT, allow_redirects=True, headers={ + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127 Safari/537.36" + }) + out.update({ + "status": resp.status_code, + "final_url": resp.url, + "headers": {k: v for k, v in resp.headers.items() if k.lower() in {"server", "content-type", "x-frame-options", "strict-transport-security"}}, + }) + except requests.RequestException as e: + out["error"] = str(e) + return out diff --git a/redcalibur/osint/user_identity/username_lookup.py b/redcalibur/osint/user_identity/username_lookup.py index ed25e7c..bae7919 100755 --- a/redcalibur/osint/user_identity/username_lookup.py +++ b/redcalibur/osint/user_identity/username_lookup.py @@ -1,45 +1,72 @@ -import requests -import subprocess -import json import logging +from concurrent.futures import ThreadPoolExecutor, as_completed +from typing import Dict, List +import requests # Initialize logger logger = logging.getLogger("username_lookup") -logging.basicConfig(level=logging.DEBUG) +if not logger.handlers: + logging.basicConfig(level=logging.INFO) + +USER_AGENT = ( + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36" +) + +# Simple platform URL patterns; can be extended +PLATFORM_URLS: Dict[str, str] = { + "twitter": "https://twitter.com/{username}", + "instagram": "https://www.instagram.com/{username}/", + "github": "https://github.com/{username}", + "linkedin": "https://www.linkedin.com/in/{username}/", + "reddit": "https://www.reddit.com/user/{username}/", + "medium": "https://medium.com/@{username}", +} -def lookup_username(username, platforms): +def _probe_profile(url: str, timeout: float = 3.0) -> bool: + try: + resp = requests.get(url, headers={"User-Agent": USER_AGENT}, timeout=timeout, allow_redirects=True) + # Basic heuristic: 200 => exists; certain platforms (LinkedIn) may return 999/other codes for blocked + if resp.status_code == 200: + return True + # Handle GitHub not found page (404) + if resp.status_code == 404: + return False + # Treat other codes as not found/blocked without failing + return False + except requests.RequestException as e: + logger.debug(f"Probe error for {url}: {e}") + return False + +def lookup_username(username: str, platforms: List[str]) -> Dict[str, str]: """ - Lookup a username across multiple platforms using Sherlock. + Lookup a username across multiple platforms via HTTP probes (no external CLI). Args: - username (str): The username to search for. - platforms (list): A list of platforms to search on. + username: Username to check + platforms: List of platform keys, e.g., ["twitter","github"] Returns: - dict: A dictionary with platform names as keys and URLs as values. + dict mapping platform -> profile URL if found; otherwise 'not found'. """ - results = {} + results: Dict[str, str] = {} - try: - # Run Sherlock as a subprocess - command = ["sherlock", username, "--print-found"] - logger.debug(f"Running command: {' '.join(command)}") - process = subprocess.run(command, capture_output=True, text=True) - - if process.returncode == 0: - logger.debug("Sherlock executed successfully.") - # Parse Sherlock's output - for line in process.stdout.splitlines(): - logger.debug(f"Processing line: {line}") - for platform in platforms: - if platform in line: - results[platform] = line.strip() - else: - logger.error(f"Sherlock error: {process.stderr.strip()}") - results["error"] = process.stderr.strip() - - except Exception as e: - logger.exception("Exception occurred during username lookup.") - results["error"] = str(e) + tasks = {} + with ThreadPoolExecutor(max_workers=min(8, max(1, len(platforms)))) as ex: + for p in platforms: + template = PLATFORM_URLS.get(p.lower()) + if not template: + results[p] = "unsupported platform" + continue + url = template.format(username=username) + tasks[ex.submit(_probe_profile, url)] = (p, url) + + for fut in as_completed(tasks): + p, url = tasks[fut] + try: + exists = fut.result() + results[p] = url if exists else "not found" + except Exception as e: + results[p] = f"error: {e}" return results diff --git a/redcalibur/osint/virustotal_integration.py b/redcalibur/osint/virustotal_integration.py index 8df8082..2c5d200 100755 --- a/redcalibur/osint/virustotal_integration.py +++ b/redcalibur/osint/virustotal_integration.py @@ -1,5 +1,7 @@ import requests +DEFAULT_TIMEOUT = 8.0 + def scan_url(api_key: str, url: str): """ Scan a URL using the VirusTotal API. @@ -16,12 +18,13 @@ def scan_url(api_key: str, url: str): "url": url } - response = requests.post(vt_url, headers=headers, data=data) - if response.status_code == 200: - return response.json() - else: - print(f"Error: {response.status_code} - {response.text}") - return None + try: + response = requests.post(vt_url, headers=headers, data=data, timeout=DEFAULT_TIMEOUT) + if response.status_code == 200: + return response.json() + return {"error": "virustotal_error", "status": response.status_code, "body": response.text} + except Exception as e: + return {"error": str(e)} def get_url_report(api_key: str, url_id: str): """ @@ -36,9 +39,10 @@ def get_url_report(api_key: str, url_id: str): "x-apikey": api_key } - response = requests.get(vt_url, headers=headers) - if response.status_code == 200: - return response.json() - else: - print(f"Error: {response.status_code} - {response.text}") - return None + try: + response = requests.get(vt_url, headers=headers, timeout=DEFAULT_TIMEOUT) + if response.status_code == 200: + return response.json() + return {"error": "virustotal_error", "status": response.status_code, "body": response.text} + except Exception as e: + return {"error": str(e)} diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e3e249d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,8 @@ +import os +import sys +from pathlib import Path + +# Ensure project root is on sys.path so `import redcalibur` works +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) diff --git a/tests/test_basic_functionality.py b/tests/test_basic_functionality.py index de660b1..fcb568e 100755 --- a/tests/test_basic_functionality.py +++ b/tests/test_basic_functionality.py @@ -2,8 +2,10 @@ import sys import os -# Add the package to the path -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'redcalibur'))) +# Add project root to path so `import redcalibur` works +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +if ROOT not in sys.path: + sys.path.insert(0, ROOT) from redcalibur.osint.domain_infrastructure.whois_lookup import perform_whois_lookup, is_valid_domain from redcalibur.osint.domain_infrastructure.dns_enumeration import enumerate_dns_records From 9fdcb6cbe2707b09289b44d68b07eb08c6128104 Mon Sep 17 00:00:00 2001 From: akvnn Date: Tue, 7 Oct 2025 18:53:26 +0530 Subject: [PATCH 2/3] chore: add persistent backend scripts and systemd unit; update .gitignore --- .gitignore | 1 + deploy/systemd/redcalibur.service | 18 ++++++++++++++++++ scripts/start_api.sh | 27 +++++++++++++++++++++++++++ scripts/stop_api.sh | 22 ++++++++++++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 deploy/systemd/redcalibur.service create mode 100755 scripts/start_api.sh create mode 100755 scripts/stop_api.sh diff --git a/.gitignore b/.gitignore index e0f2afd..3a2b0cf 100755 --- a/.gitignore +++ b/.gitignore @@ -104,3 +104,4 @@ Thumbs.db *.pkl *.joblib *.model +.vercel diff --git a/deploy/systemd/redcalibur.service b/deploy/systemd/redcalibur.service new file mode 100644 index 0000000..77cea9b --- /dev/null +++ b/deploy/systemd/redcalibur.service @@ -0,0 +1,18 @@ +[Unit] +Description=RedCalibur FastAPI Service +After=network.target + +[Service] +User=akvnn +Group=akvnn +WorkingDirectory=/home/akvnn/Downloads/aa/RedCalibur +Environment=PYTHONUNBUFFERED=1 +# Optional: if you want to load .env via systemd rather than python-dotenv +#EnvironmentFile=/home/akvnn/Downloads/aa/RedCalibur/.env +ExecStart=/home/akvnn/Downloads/aa/RedCalibur/.venv/bin/python -m uvicorn api.app:app --host 0.0.0.0 --port 8000 --workers 2 +Restart=always +RestartSec=3 +TimeoutStopSec=15 + +[Install] +WantedBy=multi-user.target diff --git a/scripts/start_api.sh b/scripts/start_api.sh new file mode 100755 index 0000000..a396cd0 --- /dev/null +++ b/scripts/start_api.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)" +PID_FILE="/tmp/redcalibur_api.pid" +LOG_FILE="/tmp/redcalibur_api.log" +VENV_BIN="$ROOT_DIR/.venv/bin" +PY="$VENV_BIN/python" + +if [ ! -x "$PY" ]; then + echo "Python venv not found at $PY. Create it and install deps first." >&2 + exit 1 +fi + +if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then + echo "API already running with PID $(cat "$PID_FILE")." >&2 + exit 0 +fi + +cd "$ROOT_DIR" +# Load .env if present for environment variables +set -a +[ -f .env ] && source .env || true +set +a + +nohup "$PY" -m uvicorn api.app:app --host 0.0.0.0 --port 8000 --workers 2 >"$LOG_FILE" 2>&1 & echo $! >"$PID_FILE" +echo "API started. PID $(cat "$PID_FILE"). Logs: $LOG_FILE" diff --git a/scripts/stop_api.sh b/scripts/stop_api.sh new file mode 100755 index 0000000..5852dbd --- /dev/null +++ b/scripts/stop_api.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +PID_FILE="/tmp/redcalibur_api.pid" + +if [ ! -f "$PID_FILE" ]; then + echo "No PID file at $PID_FILE. Is the API running?" >&2 + exit 0 +fi + +PID=$(cat "$PID_FILE") +if kill -0 "$PID" 2>/dev/null; then + kill "$PID" || true + sleep 1 + if kill -0 "$PID" 2>/dev/null; then + kill -9 "$PID" || true + fi + echo "Stopped API (PID $PID)." +else + echo "Process $PID not running. Cleaning up PID file." +fi +rm -f "$PID_FILE" From 11eab005d530165e6e51881c111591a14f4d1526 Mon Sep 17 00:00:00 2001 From: akvnn Date: Tue, 7 Oct 2025 18:56:22 +0530 Subject: [PATCH 3/3] docs: Quickstart for cloned repo, scripts usage, env vars, systemd, troubleshooting; correct username lookup notes --- README.md | 118 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 100 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 60074ab..cfe2e8e 100755 --- a/README.md +++ b/README.md @@ -6,6 +6,59 @@ This project serves both as a practical cybersecurity tool and as a demonstration of applying neural networks and AI in cybersecurity for academic purposes. +--- +## 🚀 Quickstart (cloned repo) + +Prerequisites +- Python 3.10+ (3.11/3.12/3.13 supported) +- Node.js 18+ and npm + +1) Clone and enter the folder +```bash +git clone https://github.com/PraneeshRV/RedCalibur.git +cd RedCalibur +``` + +2) Create and activate a virtual environment, install deps +```bash +python3 -m venv .venv +source .venv/bin/activate +python -m pip install -U pip setuptools wheel +python -m pip install -r requirements.txt +python -m pip install -r api/requirements.txt +``` + +3) Configure environment variables (optional but recommended) +```bash +cp .env.example .env +# edit .env and add keys as needed: SHODAN_API_KEY, VIRUSTOTAL_API_KEY, GEMINI_API_KEY +``` + +4) Start the backend API (runs in background) +```bash +chmod +x scripts/*.sh +./scripts/start_api.sh +# Health check: http://127.0.0.1:8000/health +``` + +5) Start the frontend (development) +```bash +cd frontend +npm install +npm run dev +# App: http://localhost:5173 (Vite may choose 5174 if 5173 is busy) +``` + +Stop the backend +```bash +./scripts/stop_api.sh +``` + +Optional: run tests +```bash +python -m pytest tests/ +``` + --- ## ⚔️ Features @@ -32,7 +85,7 @@ RedCalibur integrates traditional red teaming techniques with modern AI, offerin * **Document Metadata Analysis**: Analyze metadata from PDF documents. * **Reverse Image Search**: Find where an image appears online (placeholder). * **Social Media Reconnaissance**: - * **Username Footprinting**: Gather information from social media handles (placeholder). + * **Username Footprinting**: Multi-platform probes via direct HTTP checks (no external CLI required). * **LinkedIn Scraping**: Scrape company and employee data (placeholder). * **Twitter OSINT**: Analyze user data and activity (placeholder). @@ -122,16 +175,14 @@ pip install -r requirements.txt ## ⚙️ Configuration ### Environment Variables -```bash -# Required for full functionality -export SHODAN_API_KEY="your_shodan_api_key" -export OPENAI_API_KEY="your_openai_api_key" -export ANTHROPIC_API_KEY="your_anthropic_api_key" - -# Optional -export REDCALIBUR_OUTPUT_DIR="./reports" -export REDCALIBUR_LOG_LEVEL="INFO" -``` +Use `.env` at the project root (copy from `.env.example`). Keys are optional but enable richer results. + +Core keys used by the current API/UI +- SHODAN_API_KEY: Enables Shodan enrichment on network scan +- VIRUSTOTAL_API_KEY: Enables full URL malware scanning; without it, a basic URL health check is used +- GEMINI_API_KEY: Enables AI summarization of recon data (Google Generative AI) + +Additional optional variables in `.env.example` are for future/extended tooling (e.g., Hunter.io, OpenAI/Anthropic); they are not required to run the local UI and core flows. ### Configuration Check ```bash @@ -284,15 +335,22 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file This repository includes a lightweight API server and a modern React dashboard. ### Backend API -1) Copy `.env.example` to `.env` and fill your keys. -2) Install and run: +1) Copy `.env.example` to `.env` and fill keys as needed (optional). +2) Install deps (see Quickstart above) and run: +Dev (foreground, auto-reload): ```bash -pip install -r requirements.txt python -m api.run ``` -API is served at http://localhost:8000 (health: `/health`). +Background (recommended local service): +```bash +chmod +x scripts/*.sh +./scripts/start_api.sh +# Stop with: ./scripts/stop_api.sh +``` + +API default: http://127.0.0.1:8000 (health: `/health`). ### Frontend In a second terminal: @@ -303,13 +361,37 @@ npm install npm run dev ``` -App is served at http://localhost:5173 and proxies `/api/*` to the backend. +App is served at http://localhost:5173 (or 5174) and proxies `/api/*` to http://127.0.0.1:8000 in development. ### UI Highlights - Neon-red cyber theme, accessible contrast, responsive layout - Domain recon (WHOIS, DNS, subdomains, SSL), AI summary and basic risk score - Network scan with optional Shodan enrichment -- Username lookup (Sherlock) +- Username lookup (direct HTTP probes; no Sherlock dependency) - URL malware scan (VirusTotal) -Note: Ensure external tools and keys are configured (Shodan, VirusTotal, Gemini, Sherlock). \ No newline at end of file +Note: Keys are optional. Shodan and VirusTotal enrich results when provided. Gemini powers AI summaries. + +### Optional: systemd service (Linux) +If you prefer the backend to run automatically on boot: + +1) Review and, if needed, edit `deploy/systemd/redcalibur.service` paths. +2) Install and enable the service: +```bash +sudo cp deploy/systemd/redcalibur.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable redcalibur +sudo systemctl start redcalibur +``` +3) Check status and logs: +```bash +systemctl status redcalibur --no-pager +journalctl -u redcalibur -n 100 --no-pager +``` + +### Troubleshooting +- Backend health check: http://127.0.0.1:8000/health +- Frontend can’t reach API in dev: ensure the backend is running and Vite proxy is active +- Port conflicts: Vite will choose another port (5174) if 5173 is busy; change API port with `--port` in `api/run.py` if needed +- Scripts say venv missing: create the venv at `.venv` and install requirements (see Quickstart) +- Missing keys: the app will still run, but some features return reduced data \ No newline at end of file