Compare commits
12 Commits
467379d3f7
...
50a20734ff
| Author | SHA1 | Date | |
|---|---|---|---|
| 50a20734ff | |||
|
|
a0700d1960 | ||
|
|
1b8dc53952 | ||
|
|
da3ffd64c9 | ||
|
|
f1c89cc8e3 | ||
|
|
2aab758e4b | ||
|
|
e09d5436ee | ||
|
|
7d14e98860 | ||
|
|
da9170f7a0 | ||
|
|
65b12d16bd | ||
|
|
1faa891fde | ||
|
|
90a65dff6b |
@@ -15,16 +15,18 @@ RUN pip install --no-cache-dir --upgrade pip && \
|
|||||||
COPY src/ /app/src/
|
COPY src/ /app/src/
|
||||||
COPY wordlists.json /app/
|
COPY wordlists.json /app/
|
||||||
COPY entrypoint.sh /app/
|
COPY entrypoint.sh /app/
|
||||||
|
COPY start.sh /app/
|
||||||
COPY config.yaml /app/
|
COPY config.yaml /app/
|
||||||
|
|
||||||
RUN useradd -m -u 1000 krawl && \
|
RUN useradd -m -u 1000 krawl && \
|
||||||
mkdir -p /app/logs /app/data /app/exports && \
|
mkdir -p /app/logs /app/data /app/exports && \
|
||||||
chown -R krawl:krawl /app && \
|
chown -R krawl:krawl /app && \
|
||||||
chmod +x /app/entrypoint.sh
|
chmod +x /app/entrypoint.sh /app/start.sh
|
||||||
|
|
||||||
EXPOSE 5000
|
EXPOSE 5010
|
||||||
|
EXPOSE 5123
|
||||||
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "5000", "--app-dir", "src"]
|
CMD ["/app/start.sh"]
|
||||||
|
|||||||
21
README.md
@@ -39,7 +39,7 @@
|
|||||||
- [Demo](#demo)
|
- [Demo](#demo)
|
||||||
- [What is Krawl?](#what-is-krawl)
|
- [What is Krawl?](#what-is-krawl)
|
||||||
- [Krawl Dashboard](#krawl-dashboard)
|
- [Krawl Dashboard](#krawl-dashboard)
|
||||||
- [Installation](#-installation)
|
- [Quickstart](#quickstart)
|
||||||
- [Docker Run](#docker-run)
|
- [Docker Run](#docker-run)
|
||||||
- [Docker Compose](#docker-compose)
|
- [Docker Compose](#docker-compose)
|
||||||
- [Kubernetes](#kubernetes)
|
- [Kubernetes](#kubernetes)
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
- [IP Reputation](#ip-reputation)
|
- [IP Reputation](#ip-reputation)
|
||||||
- [Forward Server Header](#forward-server-header)
|
- [Forward Server Header](#forward-server-header)
|
||||||
- [Additional Documentation](#additional-documentation)
|
- [Additional Documentation](#additional-documentation)
|
||||||
- [Contributing](#-contributing)
|
- [Contributing](#contributing)
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
Tip: crawl the `robots.txt` paths for additional fun
|
Tip: crawl the `robots.txt` paths for additional fun
|
||||||
@@ -88,24 +88,29 @@ You can easily expose Krawl alongside your other services to shield them from we
|
|||||||
|
|
||||||
Krawl provides a comprehensive dashboard, accessible at a **random secret path** generated at startup or at a **custom path** configured via `KRAWL_DASHBOARD_SECRET_PATH`. This keeps the dashboard hidden from attackers scanning your honeypot.
|
Krawl provides a comprehensive dashboard, accessible at a **random secret path** generated at startup or at a **custom path** configured via `KRAWL_DASHBOARD_SECRET_PATH`. This keeps the dashboard hidden from attackers scanning your honeypot.
|
||||||
|
|
||||||
The dashboard is organized in three main tabs:
|
The dashboard is organized in five tabs:
|
||||||
|
|
||||||
- **Overview** — High-level view of attack activity: an interactive map of IP origins, recent suspicious requests, and top IPs, User-Agents, and paths.
|
- **Overview**: high-level view of attack activity: an interactive map of IP origins, recent suspicious requests, and top IPs, User-Agents, and paths.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- **Attacks** — Detailed breakdown of captured credentials, honeypot triggers, and detected attack types (SQLi, XSS, path traversal, etc.) with charts and tables.
|
- **Attacks**: detailed breakdown of captured credentials, honeypot triggers, and detected attack types (SQLi, XSS, path traversal, etc.) with charts and tables.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- **IP Insight** — In-depth forensic view of a selected IP: geolocation, ISP/ASN info, reputation flags, behavioral timeline, attack type distribution, and full access history.
|
- **IP Insight**: in-depth forensic view of a selected IP: geolocation, ISP/ASN info, reputation flags, behavioral timeline, attack type distribution, and full access history.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
Additionally, after authenticating with the dashboard password, two protected tabs become available:
|
||||||
|
|
||||||
|
- **Tracked IPs**: maintain a watchlist of IP addresses you want to monitor over time.
|
||||||
|
- **IP Banlist**: manage IP bans, view detected attackers, and export the banlist in raw or IPTables format.
|
||||||
|
|
||||||
For more details, see the [Dashboard documentation](docs/dashboard.md).
|
For more details, see the [Dashboard documentation](docs/dashboard.md).
|
||||||
|
|
||||||
|
|
||||||
## 🚀 Installation
|
## Quickstart
|
||||||
|
|
||||||
### Docker Run
|
### Docker Run
|
||||||
|
|
||||||
@@ -308,7 +313,7 @@ location / {
|
|||||||
| [Wordlist](docs/wordlist.md) | Customize fake usernames, passwords, and directory listings |
|
| [Wordlist](docs/wordlist.md) | Customize fake usernames, passwords, and directory listings |
|
||||||
| [Dashboard](docs/dashboard.md) | Access and explore the real-time monitoring dashboard |
|
| [Dashboard](docs/dashboard.md) | Access and explore the real-time monitoring dashboard |
|
||||||
|
|
||||||
## 🤝 Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions welcome! Please:
|
Contributions welcome! Please:
|
||||||
1. Fork the repository
|
1. Fork the repository
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Krawl Honeypot Configuration
|
# Krawl Honeypot Configuration
|
||||||
|
|
||||||
server:
|
server:
|
||||||
port: 5000
|
port: 5010
|
||||||
delay: 100 # Response delay in milliseconds
|
delay: 100 # Response delay in milliseconds
|
||||||
|
|
||||||
# manually set the server header, if null a random one will be used.
|
# manually set the server header, if null a random one will be used.
|
||||||
@@ -20,10 +20,9 @@ canary:
|
|||||||
token_tries: 10
|
token_tries: 10
|
||||||
|
|
||||||
dashboard:
|
dashboard:
|
||||||
# if set to "null" this will Auto-generates random path if not set
|
port: 5123
|
||||||
# can be set to "/dashboard" or similar <-- note this MUST include a forward slash
|
# Set to empty string "" to serve dashboard at root "/" on its own dedicated port
|
||||||
# secret_path: super-secret-dashboard-path
|
secret_path: ""
|
||||||
secret_path: null
|
|
||||||
|
|
||||||
# Password for accessing protected dashboard panels.
|
# Password for accessing protected dashboard panels.
|
||||||
# If null, a random password will be generated and printed in the logs.
|
# If null, a random password will be generated and printed in the logs.
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
---
|
---
|
||||||
# THIS IS FOR DEVELOPMENT PURPOSES
|
|
||||||
services:
|
services:
|
||||||
krawl:
|
krawl:
|
||||||
build:
|
build:
|
||||||
@@ -7,12 +6,15 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: krawl-server
|
container_name: krawl-server
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "5010:5010" # Honeypot (crawler trap)
|
||||||
|
- "5123:5123" # Dashboard (public UI)
|
||||||
environment:
|
environment:
|
||||||
- CONFIG_LOCATION=config.yaml
|
- CONFIG_LOCATION=config.yaml
|
||||||
# Uncomment to set a custom dashboard password (auto-generated if not set)
|
# Uncomment to set a custom dashboard password (auto-generated if not set)
|
||||||
# - KRAWL_DASHBOARD_PASSWORD=your-secret-password
|
# - KRAWL_DASHBOARD_PASSWORD=your-secret-password
|
||||||
# set this to change timezone, alternatively mount /etc/timezone or /etc/localtime based on the time system management of the host environment
|
# Override ports if needed
|
||||||
|
# - HONEYPOT_PORT=5010
|
||||||
|
# - DASHBOARD_PORT=5123
|
||||||
# - TZ=${TZ}
|
# - TZ=${TZ}
|
||||||
volumes:
|
volumes:
|
||||||
- ./wordlists.json:/app/wordlists.json:ro
|
- ./wordlists.json:/app/wordlists.json:ro
|
||||||
|
|||||||
@@ -2,20 +2,182 @@
|
|||||||
|
|
||||||
Access the dashboard at `http://<server-ip>:<port>/<dashboard-path>`
|
Access the dashboard at `http://<server-ip>:<port>/<dashboard-path>`
|
||||||
|
|
||||||
The dashboard shows:
|
The Krawl dashboard is a single-page application with **5 tabs**: Overview, Attacks, IP Insight, Tracked IPs, and IP Banlist. The last two tabs are only visible after authenticating with the dashboard password.
|
||||||
- Total and unique accesses
|
|
||||||
- Suspicious activity and attack detection
|
|
||||||
- Top IPs, paths, user-agents and GeoIP localization
|
|
||||||
- Real-time monitoring
|
|
||||||
|
|
||||||
The attackers' access to the honeypot endpoint and related suspicious activities (such as failed login attempts) are logged.
|
---
|
||||||
|
|
||||||
Krawl also implements a scoring system designed to distinguish between malicious and legitimate behavior on the website.
|
## Overview
|
||||||
|
|
||||||

|
The default landing page provides a high-level summary of all traffic and suspicious activity detected by Krawl.
|
||||||
|
|
||||||
The top IP Addresses is shown along with top paths and User Agents
|
### Stats Cards
|
||||||
|
|
||||||

|
Seven metric cards are displayed at the top:
|
||||||
|
|
||||||

|
- **Total Accesses** — total number of requests received
|
||||||
|
- **Unique IPs** — distinct IP addresses observed
|
||||||
|
- **Unique Paths** — distinct request paths
|
||||||
|
- **Suspicious Accesses** — requests flagged as suspicious
|
||||||
|
- **Honeypot Caught** — requests that hit honeypot endpoints
|
||||||
|
- **Credentials Captured** — login attempts captured by the honeypot
|
||||||
|
- **Unique Attackers** — distinct IPs classified as attackers
|
||||||
|
|
||||||
|
### Search
|
||||||
|
|
||||||
|
A real-time search bar lets you search across attacks, IPs, patterns, and locations. Results are loaded dynamically as you type.
|
||||||
|
|
||||||
|
### IP Origins Map
|
||||||
|
|
||||||
|
An interactive world map (powered by Leaflet) displays the geolocation of top IP addresses. You can filter by category:
|
||||||
|
|
||||||
|
- Attackers
|
||||||
|
- Bad Crawlers
|
||||||
|
- Good Crawlers
|
||||||
|
- Regular Users
|
||||||
|
- Unknown
|
||||||
|
|
||||||
|
The number of displayed IPs is configurable (top 10, 100, 1,000, or all).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Recent Suspicious Activity
|
||||||
|
|
||||||
|
A table showing the last 10 suspicious requests with IP address, path, user-agent, and timestamp. Each entry provides actions to view the raw HTTP request or inspect the IP in detail.
|
||||||
|
|
||||||
|
### Top IP Addresses
|
||||||
|
|
||||||
|
A paginated, sortable table ranking IPs by access count. Each IP shows its category badge and can be clicked to expand inline details or open the IP Insight tab.
|
||||||
|
|
||||||
|
### Top Paths
|
||||||
|
|
||||||
|
A paginated table of the most accessed HTTP paths and their request counts.
|
||||||
|
|
||||||
|
### Top User-Agents
|
||||||
|
|
||||||
|
A paginated table of the most common user-agent strings with their frequency.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Attacks
|
||||||
|
|
||||||
|
The Attacks tab focuses on detected malicious activity, attack patterns, and captured credentials.
|
||||||
|
|
||||||
|
### Attackers by Total Requests
|
||||||
|
|
||||||
|
A paginated table listing all detected attackers ranked by total requests. Columns include IP, total requests, first seen, last seen, and location. Sortable by multiple fields.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Captured Credentials
|
||||||
|
|
||||||
|
A table of usernames and passwords captured from honeypot login forms, with timestamps. Useful for analyzing common credential stuffing patterns.
|
||||||
|
|
||||||
|
### Honeypot Triggers by IP
|
||||||
|
|
||||||
|
Shows which IPs accessed honeypot endpoints and how many times, sorted by trigger count.
|
||||||
|
|
||||||
|
### Detected Attack Types
|
||||||
|
|
||||||
|
A detailed table of individual attack detections showing IP, path, attack type classifications, user-agent, and timestamp. Each entry can be expanded to view the raw HTTP request.
|
||||||
|
|
||||||
|
### Most Recurring Attack Types
|
||||||
|
|
||||||
|
A Chart.js visualization showing the frequency distribution of detected attack categories (e.g., SQL injection, path traversal, XSS).
|
||||||
|
|
||||||
|
### Most Recurring Attack Patterns
|
||||||
|
|
||||||
|
A paginated table of specific attack patterns and their occurrence counts across all traffic.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## IP Insight
|
||||||
|
|
||||||
|
The IP Insight tab provides a deep-dive view for a single IP address. It is activated by clicking "Inspect IP" from any table in the dashboard.
|
||||||
|
|
||||||
|
### IP Information Card
|
||||||
|
|
||||||
|
Displays comprehensive details about the selected IP:
|
||||||
|
|
||||||
|
- **Activity** — total requests, first seen, last seen, last analysis timestamp
|
||||||
|
- **Geo & Network** — location, region, timezone, ISP, ASN, reverse DNS
|
||||||
|
- **Category** — classification badge (Attacker, Good Crawler, Bad Crawler, Regular User, Unknown)
|
||||||
|
|
||||||
|
### Ban & Track Actions
|
||||||
|
|
||||||
|
When authenticated, admin actions are available:
|
||||||
|
|
||||||
|
- **Ban/Unban** — immediately add or remove the IP from the banlist
|
||||||
|
- **Track/Untrack** — add the IP to your watchlist for ongoing monitoring
|
||||||
|
|
||||||
|
### Blocklist Memberships
|
||||||
|
|
||||||
|
Shows which threat intelligence blocklists the IP appears on, providing external reputation context.
|
||||||
|
|
||||||
|
### Access Logs
|
||||||
|
|
||||||
|
A filtered view of all requests made by this specific IP, with full request details.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tracked IPs
|
||||||
|
|
||||||
|
> Requires authentication with the dashboard password.
|
||||||
|
|
||||||
|
The Tracked IPs tab lets you maintain a watchlist of IP addresses you want to monitor over time.
|
||||||
|
|
||||||
|
### Track New IP
|
||||||
|
|
||||||
|
A form to add any IP address to your tracking list for ongoing observation.
|
||||||
|
|
||||||
|
### Currently Tracked IPs
|
||||||
|
|
||||||
|
A paginated table of all manually tracked IPs, with the option to untrack each one.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## IP Banlist
|
||||||
|
|
||||||
|
> Requires authentication with the dashboard password.
|
||||||
|
|
||||||
|
The IP Banlist tab provides tools for managing IP bans. Bans are exported every 5 minutes.
|
||||||
|
|
||||||
|
### Force Ban IP
|
||||||
|
|
||||||
|
A form to immediately ban any IP address by entering it manually.
|
||||||
|
|
||||||
|
### Detected Attackers
|
||||||
|
|
||||||
|
A paginated list of all IPs detected as attackers, with quick-ban actions for each entry.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Active Ban Overrides
|
||||||
|
|
||||||
|
A table of currently active manual ban overrides, with options to unban or reset the override status for each IP.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Export Banlist
|
||||||
|
|
||||||
|
A dropdown menu to download the current banlist in two formats:
|
||||||
|
|
||||||
|
- **Raw IPs List** — plain text, one IP per line
|
||||||
|
- **IPTables Rules** — ready-to-use firewall rules
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
The dashboard uses session-based authentication with secure HTTP-only cookies. Protected features (Tracked IPs, IP Banlist, ban/track actions) require entering the dashboard password. The login includes brute-force protection with IP-based rate limiting and exponential backoff.
|
||||||
|
|
||||||
|
Click the lock icon in the top-right corner of the navigation bar to authenticate or log out.
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
@@ -2,8 +2,8 @@ apiVersion: v2
|
|||||||
name: krawl-chart
|
name: krawl-chart
|
||||||
description: A Helm chart for Krawl honeypot server
|
description: A Helm chart for Krawl honeypot server
|
||||||
type: application
|
type: application
|
||||||
version: 1.1.7
|
version: 1.2.0
|
||||||
appVersion: 1.1.7
|
appVersion: 1.2.0
|
||||||
keywords:
|
keywords:
|
||||||
- honeypot
|
- honeypot
|
||||||
- security
|
- security
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ A Helm chart for deploying the Krawl honeypot application on Kubernetes.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
helm install krawl oci://ghcr.io/blessedrebus/krawl-chart \
|
helm install krawl oci://ghcr.io/blessedrebus/krawl-chart \
|
||||||
--version 1.1.3 \
|
--version 1.2.0 \
|
||||||
--namespace krawl-system \
|
--namespace krawl-system \
|
||||||
--create-namespace \
|
--create-namespace \
|
||||||
-f values.yaml # optional
|
-f values.yaml # optional
|
||||||
@@ -170,7 +170,7 @@ kubectl get secret krawl-server -n krawl-system \
|
|||||||
You can override individual values with `--set` without a values file:
|
You can override individual values with `--set` without a values file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
helm install krawl oci://ghcr.io/blessedrebus/krawl-chart --version 1.1.3 \
|
helm install krawl oci://ghcr.io/blessedrebus/krawl-chart --version 1.2.0 \
|
||||||
--set ingress.hosts[0].host=honeypot.example.com \
|
--set ingress.hosts[0].host=honeypot.example.com \
|
||||||
--set config.canary.token_url=https://canarytokens.com/your-token
|
--set config.canary.token_url=https://canarytokens.com/your-token
|
||||||
```
|
```
|
||||||
@@ -178,7 +178,7 @@ helm install krawl oci://ghcr.io/blessedrebus/krawl-chart --version 1.1.3 \
|
|||||||
## Upgrading
|
## Upgrading
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
helm upgrade krawl oci://ghcr.io/blessedrebus/krawl-chart --version 1.1.3 -f values.yaml
|
helm upgrade krawl oci://ghcr.io/blessedrebus/krawl-chart --version 1.2.0 -f values.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Uninstalling
|
## Uninstalling
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ replicaCount: 1
|
|||||||
image:
|
image:
|
||||||
repository: ghcr.io/blessedrebus/krawl
|
repository: ghcr.io/blessedrebus/krawl
|
||||||
pullPolicy: Always
|
pullPolicy: Always
|
||||||
tag: "1.1.3"
|
tag: "1.2.0"
|
||||||
|
|
||||||
imagePullSecrets: []
|
imagePullSecrets: []
|
||||||
nameOverride: "krawl"
|
nameOverride: "krawl"
|
||||||
|
|||||||
BIN
img/attack_types_dashboard.png
Normal file
|
After Width: | Height: | Size: 283 KiB |
BIN
img/auth_prompt.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
img/banlist_attackers_dashboard.png
Normal file
|
After Width: | Height: | Size: 173 KiB |
BIN
img/banlist_overrides_dashboard.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 343 KiB After Width: | Height: | Size: 808 KiB |
BIN
img/overview_tables_dashboard.png
Normal file
|
After Width: | Height: | Size: 290 KiB |
BIN
img/top_attackers_dashboard.png
Normal file
|
After Width: | Height: | Size: 312 KiB |
BIN
img/tracked_ips_dashboard.png
Normal file
|
After Width: | Height: | Size: 131 KiB |
38
src/app.py
@@ -10,7 +10,6 @@ import os
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from fastapi import FastAPI, Request, Response
|
from fastapi import FastAPI, Request, Response
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
|
|
||||||
from config import get_config
|
from config import get_config
|
||||||
from tracker import AccessTracker, set_tracker
|
from tracker import AccessTracker, set_tracker
|
||||||
@@ -72,21 +71,7 @@ async def lifespan(app: FastAPI):
|
|||||||
tasks_master = get_tasksmaster()
|
tasks_master = get_tasksmaster()
|
||||||
tasks_master.run_scheduled_tasks()
|
tasks_master.run_scheduled_tasks()
|
||||||
|
|
||||||
password_line = ""
|
app_logger.info(f"Starting honeypot deception server on port {config.port}...")
|
||||||
if config.dashboard_password_generated:
|
|
||||||
password_line = (
|
|
||||||
f"\n\nDASHBOARD PASSWORD (auto-generated)\n{config.dashboard_password}"
|
|
||||||
)
|
|
||||||
|
|
||||||
banner = f"""
|
|
||||||
|
|
||||||
============================================================
|
|
||||||
DASHBOARD AVAILABLE AT
|
|
||||||
{config.dashboard_secret_path}{password_line}
|
|
||||||
============================================================
|
|
||||||
"""
|
|
||||||
app_logger.info(banner)
|
|
||||||
app_logger.info(f"Starting deception server on port {config.port}...")
|
|
||||||
if config.canary_token_url:
|
if config.canary_token_url:
|
||||||
app_logger.info(
|
app_logger.info(
|
||||||
f"Canary token will appear after {config.canary_token_tries} tries"
|
f"Canary token will appear after {config.canary_token_tries} tries"
|
||||||
@@ -155,29 +140,10 @@ def create_app() -> FastAPI:
|
|||||||
access_logger.info(f"[{method}] {client_ip} - {path} - {status}")
|
access_logger.info(f"[{method}] {client_ip} - {path} - {status}")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Mount static files for the dashboard
|
|
||||||
config = get_config()
|
|
||||||
secret = config.dashboard_secret_path.lstrip("/")
|
|
||||||
static_dir = os.path.join(os.path.dirname(__file__), "templates", "static")
|
|
||||||
application.mount(
|
|
||||||
f"/{secret}/static",
|
|
||||||
StaticFiles(directory=static_dir),
|
|
||||||
name="dashboard-static",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Import and include routers
|
# Import and include routers
|
||||||
from routes.honeypot import router as honeypot_router
|
from routes.honeypot import router as honeypot_router
|
||||||
from routes.api import router as api_router
|
|
||||||
from routes.dashboard import router as dashboard_router
|
|
||||||
from routes.htmx import router as htmx_router
|
|
||||||
|
|
||||||
# Dashboard/API/HTMX routes (prefixed with secret path, before honeypot catch-all)
|
# Honeypot routes (catch-all)
|
||||||
dashboard_prefix = f"/{secret}"
|
|
||||||
application.include_router(dashboard_router, prefix=dashboard_prefix)
|
|
||||||
application.include_router(api_router, prefix=dashboard_prefix)
|
|
||||||
application.include_router(htmx_router, prefix=dashboard_prefix)
|
|
||||||
|
|
||||||
# Honeypot routes (catch-all must be last)
|
|
||||||
application.include_router(honeypot_router)
|
application.include_router(honeypot_router)
|
||||||
|
|
||||||
return application
|
return application
|
||||||
|
|||||||
94
src/app_dashboard.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
FastAPI application for the Krawl dashboard.
|
||||||
|
Runs on a dedicated port (default 5123) and serves the dashboard at /.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Request, Response
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
|
from config import get_config
|
||||||
|
from database import initialize_database
|
||||||
|
from tasks_master import get_tasksmaster
|
||||||
|
from logger import initialize_logging, get_app_logger
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
"""Dashboard application startup and shutdown lifecycle."""
|
||||||
|
config = get_config()
|
||||||
|
# Force dashboard path to empty (root) for this dedicated app
|
||||||
|
config.dashboard_secret_path = ""
|
||||||
|
|
||||||
|
initialize_logging(log_level=config.log_level)
|
||||||
|
app_logger = get_app_logger()
|
||||||
|
|
||||||
|
try:
|
||||||
|
app_logger.info(f"Initializing database at: {config.database_path}")
|
||||||
|
initialize_database(config.database_path)
|
||||||
|
app_logger.info("Database ready")
|
||||||
|
except Exception as e:
|
||||||
|
app_logger.warning(
|
||||||
|
f"Database initialization failed: {e}. Continuing with in-memory only."
|
||||||
|
)
|
||||||
|
|
||||||
|
app.state.config = config
|
||||||
|
|
||||||
|
# Start scheduled tasks (handles cache warming, backups, etc.)
|
||||||
|
tasks_master = get_tasksmaster()
|
||||||
|
tasks_master.run_scheduled_tasks()
|
||||||
|
|
||||||
|
password_line = ""
|
||||||
|
if config.dashboard_password_generated:
|
||||||
|
password_line = (
|
||||||
|
f"\n\nDASHBOARD PASSWORD (auto-generated)\n{config.dashboard_password}"
|
||||||
|
)
|
||||||
|
|
||||||
|
banner = f"""
|
||||||
|
|
||||||
|
============================================================
|
||||||
|
DASHBOARD AVAILABLE AT http://0.0.0.0:{config.dashboard_port}/{password_line}
|
||||||
|
============================================================
|
||||||
|
"""
|
||||||
|
app_logger.info(banner)
|
||||||
|
app_logger.info(f"Starting dashboard server on port {config.dashboard_port}...")
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
app_logger.info("Dashboard server shutting down...")
|
||||||
|
|
||||||
|
|
||||||
|
def create_dashboard_app() -> FastAPI:
|
||||||
|
"""Create and configure the dashboard FastAPI application."""
|
||||||
|
application = FastAPI(
|
||||||
|
docs_url=None,
|
||||||
|
redoc_url=None,
|
||||||
|
openapi_url=None,
|
||||||
|
lifespan=lifespan,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mount static files at /static
|
||||||
|
static_dir = os.path.join(os.path.dirname(__file__), "templates", "static")
|
||||||
|
application.mount(
|
||||||
|
"/static",
|
||||||
|
StaticFiles(directory=static_dir),
|
||||||
|
name="dashboard-static",
|
||||||
|
)
|
||||||
|
|
||||||
|
from routes.api import router as api_router
|
||||||
|
from routes.dashboard import router as dashboard_router
|
||||||
|
from routes.htmx import router as htmx_router
|
||||||
|
|
||||||
|
# All dashboard routes at root prefix
|
||||||
|
application.include_router(dashboard_router, prefix="")
|
||||||
|
application.include_router(api_router, prefix="")
|
||||||
|
application.include_router(htmx_router, prefix="")
|
||||||
|
|
||||||
|
return application
|
||||||
|
|
||||||
|
|
||||||
|
app = create_dashboard_app()
|
||||||
@@ -18,7 +18,8 @@ import yaml
|
|||||||
class Config:
|
class Config:
|
||||||
"""Configuration class for the deception server"""
|
"""Configuration class for the deception server"""
|
||||||
|
|
||||||
port: int = 5000
|
port: int = 5010
|
||||||
|
dashboard_port: int = 5123
|
||||||
delay: int = 100 # milliseconds
|
delay: int = 100 # milliseconds
|
||||||
server_header: str = ""
|
server_header: str = ""
|
||||||
links_length_range: Tuple[int, int] = (5, 15)
|
links_length_range: Tuple[int, int] = (5, 15)
|
||||||
@@ -173,6 +174,8 @@ class Config:
|
|||||||
dashboard_path = dashboard.get("secret_path")
|
dashboard_path = dashboard.get("secret_path")
|
||||||
if dashboard_path is None:
|
if dashboard_path is None:
|
||||||
dashboard_path = f"/{os.urandom(16).hex()}"
|
dashboard_path = f"/{os.urandom(16).hex()}"
|
||||||
|
elif dashboard_path == "":
|
||||||
|
dashboard_path = ""
|
||||||
else:
|
else:
|
||||||
# ensure the dashboard path starts with a /
|
# ensure the dashboard path starts with a /
|
||||||
if dashboard_path[:1] != "/":
|
if dashboard_path[:1] != "/":
|
||||||
@@ -186,7 +189,8 @@ class Config:
|
|||||||
dashboard_password_generated = True
|
dashboard_password_generated = True
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
port=server.get("port", 5000),
|
port=server.get("port", 5010),
|
||||||
|
dashboard_port=dashboard.get("port", 5123),
|
||||||
delay=server.get("delay", 100),
|
delay=server.get("delay", 100),
|
||||||
server_header=server.get("server_header", ""),
|
server_header=server.get("server_header", ""),
|
||||||
links_length_range=(
|
links_length_range=(
|
||||||
|
|||||||
@@ -254,7 +254,13 @@ async def all_ips(
|
|||||||
page_size = min(max(1, page_size), 10000)
|
page_size = min(max(1, page_size), 10000)
|
||||||
|
|
||||||
# Serve from cache on default map request (top 100 IPs)
|
# Serve from cache on default map request (top 100 IPs)
|
||||||
if page == 1 and page_size == 100 and sort_by == "total_requests" and sort_order == "desc" and is_warm():
|
if (
|
||||||
|
page == 1
|
||||||
|
and page_size == 100
|
||||||
|
and sort_by == "total_requests"
|
||||||
|
and sort_order == "desc"
|
||||||
|
and is_warm()
|
||||||
|
):
|
||||||
cached = get_cached("map_ips")
|
cached = get_cached("map_ips")
|
||||||
if cached:
|
if cached:
|
||||||
return JSONResponse(content=cached, headers=_no_cache_headers())
|
return JSONResponse(content=cached, headers=_no_cache_headers())
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ router = APIRouter()
|
|||||||
@router.get("/")
|
@router.get("/")
|
||||||
async def dashboard_page(request: Request):
|
async def dashboard_page(request: Request):
|
||||||
config = request.app.state.config
|
config = request.app.state.config
|
||||||
dashboard_path = "/" + config.dashboard_secret_path.lstrip("/")
|
_path = config.dashboard_secret_path.strip("/")
|
||||||
|
dashboard_path = f"/{_path}" if _path else ""
|
||||||
|
|
||||||
# Serve from pre-computed cache when available, fall back to live queries
|
# Serve from pre-computed cache when available, fall back to live queries
|
||||||
if is_warm():
|
if is_warm():
|
||||||
@@ -50,7 +51,8 @@ async def ip_page(ip_address: str, request: Request):
|
|||||||
try:
|
try:
|
||||||
stats = db.get_ip_stats_by_ip(ip_address)
|
stats = db.get_ip_stats_by_ip(ip_address)
|
||||||
config = request.app.state.config
|
config = request.app.state.config
|
||||||
dashboard_path = "/" + config.dashboard_secret_path.lstrip("/")
|
_path = config.dashboard_secret_path.strip("/")
|
||||||
|
dashboard_path = f"/{_path}" if _path else ""
|
||||||
|
|
||||||
if stats:
|
if stats:
|
||||||
# Transform fields for template compatibility
|
# Transform fields for template compatibility
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ router = APIRouter()
|
|||||||
|
|
||||||
def _dashboard_path(request: Request) -> str:
|
def _dashboard_path(request: Request) -> str:
|
||||||
config = request.app.state.config
|
config = request.app.state.config
|
||||||
return "/" + config.dashboard_secret_path.lstrip("/")
|
path = config.dashboard_secret_path.strip("/")
|
||||||
|
return f"/{path}" if path else ""
|
||||||
|
|
||||||
|
|
||||||
# ── Honeypot Triggers ────────────────────────────────────────────────
|
# ── Honeypot Triggers ────────────────────────────────────────────────
|
||||||
@@ -60,7 +61,11 @@ async def htmx_top_ips(
|
|||||||
sort_order: str = Query("desc"),
|
sort_order: str = Query("desc"),
|
||||||
):
|
):
|
||||||
# Serve from cache on default first-page request
|
# Serve from cache on default first-page request
|
||||||
cached = get_cached("top_ips") if (page == 1 and sort_by == "count" and sort_order == "desc" and is_warm()) else None
|
cached = (
|
||||||
|
get_cached("top_ips")
|
||||||
|
if (page == 1 and sort_by == "count" and sort_order == "desc" and is_warm())
|
||||||
|
else None
|
||||||
|
)
|
||||||
if cached:
|
if cached:
|
||||||
result = cached
|
result = cached
|
||||||
else:
|
else:
|
||||||
@@ -93,7 +98,11 @@ async def htmx_top_paths(
|
|||||||
sort_by: str = Query("count"),
|
sort_by: str = Query("count"),
|
||||||
sort_order: str = Query("desc"),
|
sort_order: str = Query("desc"),
|
||||||
):
|
):
|
||||||
cached = get_cached("top_paths") if (page == 1 and sort_by == "count" and sort_order == "desc" and is_warm()) else None
|
cached = (
|
||||||
|
get_cached("top_paths")
|
||||||
|
if (page == 1 and sort_by == "count" and sort_order == "desc" and is_warm())
|
||||||
|
else None
|
||||||
|
)
|
||||||
if cached:
|
if cached:
|
||||||
result = cached
|
result = cached
|
||||||
else:
|
else:
|
||||||
@@ -126,7 +135,11 @@ async def htmx_top_ua(
|
|||||||
sort_by: str = Query("count"),
|
sort_by: str = Query("count"),
|
||||||
sort_order: str = Query("desc"),
|
sort_order: str = Query("desc"),
|
||||||
):
|
):
|
||||||
cached = get_cached("top_ua") if (page == 1 and sort_by == "count" and sort_order == "desc" and is_warm()) else None
|
cached = (
|
||||||
|
get_cached("top_ua")
|
||||||
|
if (page == 1 and sort_by == "count" and sort_order == "desc" and is_warm())
|
||||||
|
else None
|
||||||
|
)
|
||||||
if cached:
|
if cached:
|
||||||
result = cached
|
result = cached
|
||||||
else:
|
else:
|
||||||
|
|||||||
17
start.sh
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
HONEYPOT_PORT=${HONEYPOT_PORT:-5010}
|
||||||
|
DASHBOARD_PORT=${DASHBOARD_PORT:-5123}
|
||||||
|
|
||||||
|
echo "Starting Krawl honeypot on port $HONEYPOT_PORT..."
|
||||||
|
uvicorn app:app --host 0.0.0.0 --port "$HONEYPOT_PORT" --app-dir src &
|
||||||
|
HONEYPOT_PID=$!
|
||||||
|
|
||||||
|
echo "Starting Krawl dashboard on port $DASHBOARD_PORT..."
|
||||||
|
uvicorn app_dashboard:app --host 0.0.0.0 --port "$DASHBOARD_PORT" --app-dir src &
|
||||||
|
DASHBOARD_PID=$!
|
||||||
|
|
||||||
|
# Wait for either process to exit; if one dies, kill the other
|
||||||
|
wait -n 2>/dev/null || wait
|
||||||
|
kill $HONEYPOT_PID $DASHBOARD_PID 2>/dev/null || true
|
||||||