First commit

This commit is contained in:
BlessedRebuS
2025-12-14 19:08:01 +01:00
parent 5e46841858
commit 076b2de584
43 changed files with 3410 additions and 2 deletions

14
.dockerignore Normal file
View File

@@ -0,0 +1,14 @@
__pycache__
*.pyc
*.pyo
*.pyd
.Python
*.so
*.egg
*.egg-info
dist
build
.git
.gitignore
README.md
*.md

71
.gitignore vendored Normal file
View File

@@ -0,0 +1,71 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual Environment
venv/
env/
ENV/
env.bak/
venv.bak/
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Helm
helm/charts/
helm/*.tgz
helm/values-production.yaml
helm/values-*.yaml
!helm/values.yaml
# Kubernetes secrets (if generated locally)
*.secret.yaml
secrets/
# Docker
*.log
# Environment variables
.env
.env.local
.env.*.local
# Logs
*.log
logs/
# Temporary files
*.tmp
*.temp
.cache/
# Personal canary tokens or sensitive configs
*canary*token*.yaml
personal-values.yaml

19
Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
FROM python:3.11-slim
LABEL org.opencontainers.image.source=https://github.com/BlessedRebuS/Krawl
WORKDIR /app
COPY src/ /app/src/
COPY wordlists.json /app/
RUN useradd -m -u 1000 krawl && \
chown -R krawl:krawl /app
USER krawl
EXPOSE 5000
ENV PYTHONUNBUFFERED=1
CMD ["python3", "src/server.py"]

299
README.md
View File

@@ -1,2 +1,297 @@
# Krawl
Krawl is a Web Honeypot & Deception server that aims to foul enumerations, web crawling, fuzzing and bruteforcing
<h1 align="center">🕷️ Krawl</h1>
<p align="center">
A modern, customizable zero-dependencies honeypot server designed to detect and track malicious activity through deceptive web pages, fake credentials, and canary tokens.
</p>
<div align="center">
<a href="https://github.com/blessedrebus/krawl/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/blessedrebus/krawl" alt="License">
</a>
<a href="https://github.com/blessedrebus/krawl/releases">
<img src="https://img.shields.io/github/v/release/blessedrebus/krawl" alt="Release">
</a>
</div>
<div align="center">
<a href="https://ghcr.io/blessedrebus/krawl">
<img src="https://img.shields.io/badge/ghcr.io-krawl-blue" alt="GitHub Container Registry">
</a>
<a href="https://kubernetes.io/">
<img src="https://img.shields.io/badge/kubernetes-ready-326CE5?logo=kubernetes&logoColor=white" alt="Kubernetes">
</a>
<a href="https://github.com/BlessedRebuS/Krawl/pkgs/container/krawl-chart">
<img src="https://img.shields.io/badge/helm-chart-0F1689?logo=helm&logoColor=white" alt="Helm Chart">
</a>
</div>
<br>
<p align="center">
<a href="#-overview">Overview</a> •
<a href="#-quick-start">Quick Start</a> •
<a href="#%EF%B8%8F-configuration">Configuration</a> •
<a href="#-dashboard">Dashboard</a> •
<a href="#-deception-techniques">Deception Techniques</a> •
<a href="#-contributing">Contributing</a>
</p>
![asd](img/deception-page.png)
## What is Krawl?
Krawl is a simple cloud native deception server that creates fake web applications with low hanging fruit and juicy fake random information.
It features:
- **Spider Trap Pages**: Infinite random links to waste crawler resources based on the [spidertrap project](https://github.com/adhdproject/spidertrap)
- **Fake Login Pages**: WordPress, phpMyAdmin, admin panels
- **Honeypot Paths**: Advertised in robots.txt to catch scanners
- **Fake Credentials**: Realistic-looking usernames, passwords, API keys
- **Canary Token Integration**: External alert triggering
- **Real-time Dashboard**: Monitor suspicious activity
- **Customizable Wordlists**: Easy JSON-based configuration
- **Random Error Injection**: Mimic real server behavior
## 🚀 Quick Start
## Helm Chart
Install with default values
```bash
helm install krawl ./helm \
--namespace krawl-system \
--create-namespace
```
Install with custom values
```bash
helm install krawl ./helm \
--namespace krawl-system \
--create-namespace \
--values values.yaml
```
Install with custom canary token
```bash
helm install krawl ./helm \
--namespace krawl-system \
--create-namespace \
--set config.canaryTokenUrl="http://your-canary-token-url"
```
Uninstall with
```bash
helm uninstall krawl --namespace krawl-system
```
## Kubernetes / Kustomize
Apply all manifests
```bash
kubectl apply -k manifests/
```
Retrieve dashboard path
```bash
kubectl get secret krawl-server -n krawl-system -o jsonpath='{.data.dashboard-path}' | base64 -d
```
Uninstall with
```bash
kubectl delete -k manifests/
```
## Docker
```bash
docker run -d \
-p 5000:5000 \
-e CANARY_TOKEN_URL="http://your-canary-token-url" \
--name krawl \
ghcr.io/blessedrebus/krawl:latest
```
## Docker Compose
```bash
docker-compose up -d
```
## Python 3.11+
Clone the repository
```bash
git clone https://github.com/blessedrebus/krawl.git
cd krawl/src
```
Run the server
```bash
python3 server.py
```
Visit
`http://localhost:5000`
To access the dashboard
`http://localhost:5000/dashboard-secret-path`
## Configuration via Environment Variables
To customize the deception server installation several **environment variables** can be specified.
| Variable | Description | Default |
|----------|-------------|---------|
| `PORT` | Server listening port | `5000` |
| `DELAY` | Response delay in milliseconds | `100` |
| `LINKS_MIN_LENGTH` | Minimum random link length | `5` |
| `LINKS_MAX_LENGTH` | Maximum random link length | `15` |
| `LINKS_MIN_PER_PAGE` | Minimum links per page | `10` |
| `LINKS_MAX_PER_PAGE` | Maximum links per page | `15` |
| `MAX_COUNTER` | Initial counter value | `10` |
| `CANARY_TOKEN_TRIES` | Requests before showing canary token | `10` |
| `CANARY_TOKEN_URL` | External canary token URL | None |
| `DASHBOARD_SECRET_PATH` | Custom dashboard path | Auto-generated |
| `PROBABILITY_ERROR_CODES` | Error response probability (0-100%) | `0` |
## robots.txt
The actual (juicy) robots.txt configuration is the following
```txt
Disallow: /admin/
Disallow: /api/
Disallow: /backup/
Disallow: /config/
Disallow: /database/
Disallow: /private/
Disallow: /uploads/
Disallow: /wp-admin/
Disallow: /phpMyAdmin/
Disallow: /admin/login.php
Disallow: /api/v1/users
Disallow: /api/v2/secrets
Disallow: /.env
Disallow: /credentials.txt
Disallow: /passwords.txt
Disallow: /.git/
Disallow: /backup.sql
Disallow: /db_backup.sql
```
## Wordlists Customization
Edit `wordlists.json` to customize fake data:
```json
{
"usernames": {
"prefixes": ["admin", "root", "user"],
"suffixes": ["_prod", "_dev", "123"]
},
"passwords": {
"prefixes": ["P@ssw0rd", "Admin"],
"simple": ["test", "password"]
},
"directory_listing": {
"files": ["credentials.txt", "backup.sql"],
"directories": ["admin/", "backup/"]
}
}
```
or **values.yaml** in the case of helm chart installation
## Dashboard
Access the dashboard at `http://<server-ip>:<port>/<dashboard-path>`
The attackers' triggered honeypot path and the suspicious activity (such as failed login attempts) are logged
![asd](img/dashboard-1.png)
The top IP Addresses is shown along with top paths and User Agents
![asd](img/dashboard-2.png)
The dashboard shows:
- Total and unique accesses
- Suspicious activity detection
- Honeypot triggers
- Top IPs, paths, and user-agents
- Real-time monitoring
### Retrieving Dashboard Path
Check server startup logs
**Python/Docker:**
```bash
docker logs krawl | grep "Dashboard available"
```
**Kubernetes:**
```bash
kubectl get secret krawl-server -n krawl-system \
-o jsonpath='{.data.dashboard-path}' | base64 -d && echo
```
**Helm:**
```bash
kubectl get secret krawl -n krawl-system \
-o jsonpath='{.data.dashboard-path}' | base64 -d && echo
```
## Deception Techniques
### 1. Robots.txt Honeypots
Advertises forbidden paths that legitimate crawlers avoid but scanners investigate:
- `/admin/`, `/backup/`, `/config/`
- `/credentials.txt`, `/.env`, `/passwords.txt`
### 2. Fake Services
Mimics real applications:
- WordPress (`/wp-admin`, `/wp-login.php`)
- phpMyAdmin (`/phpmyadmin`)
- Admin panels (`/admin`, `/login`)
### 3. Credential Traps
Generates realistic but fake:
- Usernames and passwords
- API keys and tokens
- Database connection strings
- AWS credentials
### 4. Spider Traps
Infinite random links to waste automated scanner time
### 5. Error Simulation
Random HTTP errors to appear more realistic
### Custom Canary Token
Generate a canary token at [canarytokens.org](https://canarytokens.org) and configure:
```bash
export CANARY_TOKEN_URL="http://canarytokens.com/..."
python3 src/server.py
```
## Contributing
Contributions welcome! Please:
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Submit a pull request
<div align="center">
## Disclaimer
**This is a deception/honeypot system.**
Deploy in isolated environments and monitor carefully for security events.
Use responsibly and in compliance with applicable laws and regulations.

60
deployment.yaml Normal file
View File

@@ -0,0 +1,60 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: krawl-server
namespace: krawl
labels:
app: krawl-server
spec:
replicas: 1
selector:
matchLabels:
app: krawl-server
template:
metadata:
labels:
app: krawl-server
spec:
containers:
- name: krawl
image: ghcr.io/blessedrebus/krawl:latest
imagePullPolicy: Always
ports:
- containerPort: 5000
name: http
protocol: TCP
envFrom:
- configMapRef:
name: krawl-config
volumeMounts:
- name: wordlists
mountPath: /app/wordlists.json
subPath: wordlists.json
readOnly: true
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /
port: 5000
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: 5000
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
volumes:
- name: wordlists
configMap:
name: krawl-wordlists

33
docker-compose.yaml Normal file
View File

@@ -0,0 +1,33 @@
version: '3.8'
services:
krawl:
build:
context: .
dockerfile: Dockerfile
container_name: krawl-server
ports:
- "5000:5000"
volumes:
- ./wordlists.json:/app/wordlists.json:ro
environment:
- PORT=5000
- DELAY=100
- LINKS_MIN_LENGTH=5
- LINKS_MAX_LENGTH=15
- LINKS_MIN_PER_PAGE=10
- LINKS_MAX_PER_PAGE=15
- MAX_COUNTER=10
- CANARY_TOKEN_TRIES=10
- PROBABILITY_ERROR_CODES=0
# Optional: Set your canary token URL
# - CANARY_TOKEN_URL=http://canarytokens.com/api/users/YOUR_TOKEN/passwords.txt
# Optional: Set custom dashboard path (auto-generated if not set)
# - DASHBOARD_SECRET_PATH=/my-secret-dashboard
restart: unless-stopped
healthcheck:
test: ["CMD", "python3", "-c", "import requests; requests.get('http://localhost:5000')"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s

1
helm/.helmignore Normal file
View File

@@ -0,0 +1 @@
.helmignore

15
helm/Chart.yaml Normal file
View File

@@ -0,0 +1,15 @@
apiVersion: v2
name: krawl-chart
description: A Helm chart for Krawl honeypot server
type: application
version: 0.1.2
appVersion: "1.0.0"
keywords:
- honeypot
- security
- krawl
maintainers:
- name: blessedrebus
home: https://github.com/blessedrebus/krawl
sources:
- https://github.com/blessedrebus/krawl

60
helm/NOTES.txt Normal file
View File

@@ -0,0 +1,60 @@
▄▄▄ ▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄ ▄▄▄▄ ▄▄▄ ▄▄▄▄ ▄▄▄
███ ▄███▀ ███▀▀███▄ ▄██▀▀██▄ ▀███ ███ ███▀ ███
███████ ███▄▄███▀ ███ ███ ███ ███ ███ ███
███▀███▄ ███▀▀██▄ ███▀▀███ ███▄▄███▄▄███ ███
███ ▀███ ███ ▀███ ███ ███ ▀████▀████▀ ████████
|
|
|
|
|
|| ||
\\(_)//
//(___)\\
|| ||
WARNING: This is a krawl/honeypot service. Monitor access logs for security events.
For more information, visit: https://github.com/blessedrebus/krawl
Your krawl honeypot server has been deployed successfully.
{{- if .Values.service.type }}
Service Type: {{ .Values.service.type }}
{{- if eq .Values.service.type "LoadBalancer" }}
To get the LoadBalancer IP address, run:
kubectl get svc {{ include "krawl.fullname" . }} -n {{ .Release.Namespace }}
Once the EXTERNAL-IP is assigned, access your krawl server at:
http://<EXTERNAL-IP>:{{ .Values.service.port }}
{{- else if eq .Values.service.type "NodePort" }}
To get the NodePort, run:
export NODE_PORT=$(kubectl get svc {{ include "krawl.fullname" . }} -n {{ .Release.Namespace }} -o jsonpath='{.spec.ports[0].nodePort}')
export NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[0].address}')
echo "Access at: http://$NODE_IP:$NODE_PORT"
{{- else if eq .Values.service.type "ClusterIP" }}
To access the service from your local machine:
kubectl port-forward svc/{{ include "krawl.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }} -n {{ .Release.Namespace }}
Then access at: http://localhost:{{ .Values.service.port }}
{{- end }}
{{- end }}
Dashboard Access:
To retrieve the dashboard path, run:
kubectl get secret {{ include "krawl.fullname" . }} -n {{ .Release.Namespace }} -o jsonpath='{.data.dashboard-path}' | base64 -d && echo
Then access the dashboard at:
http://<EXTERNAL-IP>:{{ .Values.service.port }}/<dashboard-path>
{{- if .Values.ingress.enabled }}
Ingress is ENABLED. Your service will be available at:
{{- range .Values.ingress.hosts }}
- http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }}
{{- end }}
{{- end }}

60
helm/templates/NOTES.txt Normal file
View File

@@ -0,0 +1,60 @@
▄▄▄ ▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄ ▄▄▄▄ ▄▄▄ ▄▄▄▄ ▄▄▄
███ ▄███▀ ███▀▀███▄ ▄██▀▀██▄ ▀███ ███ ███▀ ███
███████ ███▄▄███▀ ███ ███ ███ ███ ███ ███
███▀███▄ ███▀▀██▄ ███▀▀███ ███▄▄███▄▄███ ███
███ ▀███ ███ ▀███ ███ ███ ▀████▀████▀ ████████
|
|
|
|
|
|| ||
\\(_)//
//(___)\\
|| ||
WARNING: This is a deception/honeypot service. Monitor access logs for security events.
For more information, visit: https://github.com/blessedrebus/deception
Your deception honeypot server has been deployed successfully.
{{- if .Values.service.type }}
Service Type: {{ .Values.service.type }}
{{- if eq .Values.service.type "LoadBalancer" }}
To get the LoadBalancer IP address, run:
kubectl get svc {{ include "krawl.fullname" . }} -n {{ .Release.Namespace }}
Once the EXTERNAL-IP is assigned, access your deception server at:
http://<EXTERNAL-IP>:{{ .Values.service.port }}
{{- else if eq .Values.service.type "NodePort" }}
To get the NodePort, run:
export NODE_PORT=$(kubectl get svc {{ include "krawl.fullname" . }} -n {{ .Release.Namespace }} -o jsonpath='{.spec.ports[0].nodePort}')
export NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[0].address}')
echo "Access at: http://$NODE_IP:$NODE_PORT"
{{- else if eq .Values.service.type "ClusterIP" }}
To access the service from your local machine:
kubectl port-forward svc/{{ include "krawl.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }} -n {{ .Release.Namespace }}
Then access at: http://localhost:{{ .Values.service.port }}
{{- end }}
{{- end }}
Dashboard Access:
To retrieve the dashboard path, run:
kubectl get secret {{ include "krawl.fullname" . }} -n {{ .Release.Namespace }} -o jsonpath='{.data.dashboard-path}' | base64 -d && echo
Then access the dashboard at:
http://<EXTERNAL-IP>:{{ .Values.service.port }}/<dashboard-path>
{{- if .Values.ingress.enabled }}
Ingress is ENABLED. Your service will be available at:
{{- range .Values.ingress.hosts }}
- http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,60 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "krawl.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "krawl.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "krawl.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "krawl.labels" -}}
helm.sh/chart: {{ include "krawl.chart" . }}
{{ include "krawl.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "krawl.selectorLabels" -}}
app.kubernetes.io/name: {{ include "krawl.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "krawl.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "krawl.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,17 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "krawl.fullname" . }}-config
labels:
{{- include "krawl.labels" . | nindent 4 }}
data:
PORT: {{ .Values.config.port | quote }}
DELAY: {{ .Values.config.delay | quote }}
LINKS_MIN_LENGTH: {{ .Values.config.linksMinLength | quote }}
LINKS_MAX_LENGTH: {{ .Values.config.linksMaxLength | quote }}
LINKS_MIN_PER_PAGE: {{ .Values.config.linksMinPerPage | quote }}
LINKS_MAX_PER_PAGE: {{ .Values.config.linksMaxPerPage | quote }}
MAX_COUNTER: {{ .Values.config.maxCounter | quote }}
CANARY_TOKEN_TRIES: {{ .Values.config.canaryTokenTries | quote }}
PROBABILITY_ERROR_CODES: {{ .Values.config.probabilityErrorCodes | quote }}
CANARY_TOKEN_URL: {{ .Values.config.canaryTokenUrl | quote }}

View File

@@ -0,0 +1,84 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "krawl.fullname" . }}
labels:
{{- include "krawl.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "krawl.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "krawl.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
{{- with .Values.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.config.port }}
protocol: TCP
envFrom:
- configMapRef:
name: {{ include "krawl.fullname" . }}-config
env:
- name: DASHBOARD_SECRET_PATH
valueFrom:
secretKeyRef:
name: {{ include "krawl.fullname" . }}
key: dashboard-path
volumeMounts:
- name: wordlists
mountPath: /app/wordlists.json
subPath: wordlists.json
readOnly: true
{{- with .Values.livenessProbe }}
livenessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.readinessProbe }}
readinessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
volumes:
- name: wordlists
configMap:
name: {{ include "krawl.fullname" . }}-wordlists
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

32
helm/templates/hpa.yaml Normal file
View File

@@ -0,0 +1,32 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "krawl.fullname" . }}
labels:
{{- include "krawl.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "krawl.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,42 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "krawl.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "krawl.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "krawl.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,24 @@
{{- if .Values.networkPolicy.enabled -}}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "krawl.fullname" . }}
labels:
{{- include "krawl.labels" . | nindent 4 }}
spec:
podSelector:
matchLabels:
{{- include "krawl.selectorLabels" . | nindent 6 }}
{{- with .Values.networkPolicy.policyTypes }}
policyTypes:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.networkPolicy.ingress }}
ingress:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.networkPolicy.egress }}
egress:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,16 @@
{{- $secret := (lookup "v1" "Secret" .Release.Namespace (include "krawl.fullname" .)) -}}
{{- $dashboardPath := "" -}}
{{- if and $secret $secret.data -}}
{{- $dashboardPath = index $secret.data "dashboard-path" | b64dec -}}
{{- else -}}
{{- $dashboardPath = printf "/%s" (randAlphaNum 32) -}}
{{- end -}}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "krawl.fullname" . }}
labels:
{{- include "krawl.labels" . | nindent 4 }}
type: Opaque
stringData:
dashboard-path: {{ $dashboardPath | quote }}

View File

@@ -0,0 +1,26 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "krawl.fullname" . }}
labels:
{{- include "krawl.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
{{- if .Values.service.externalTrafficPolicy }}
externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }}
{{- end }}
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "krawl.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,9 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "krawl.fullname" . }}-wordlists
labels:
{{- include "krawl.labels" . | nindent 4 }}
data:
wordlists.json: |
{{- .Values.wordlists | toJson | nindent 4 }}

295
helm/values.yaml Normal file
View File

@@ -0,0 +1,295 @@
replicaCount: 1
image:
repository: ghcr.io/blessedrebus/krawl
pullPolicy: Always
tag: "latest"
imagePullSecrets: []
nameOverride: "krawl"
fullnameOverride: ""
serviceAccount:
create: false
annotations: {}
name: ""
podAnnotations: {}
podSecurityContext: {}
securityContext: {}
service:
type: LoadBalancer
port: 5000
annotations: {}
# Preserve source IP when using LoadBalancer
externalTrafficPolicy: Local
ingress:
enabled: true
className: "nginx"
annotations: {}
hosts:
- host: krawl.example.com
paths:
- path: /
pathType: Prefix
tls: []
# - secretName: krawl-tls
# hosts:
# - krawl.example.com
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 64Mi
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 5
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}
# Application configuration
config:
port: 5000
delay: 100
linksMinLength: 5
linksMaxLength: 15
linksMinPerPage: 10
linksMaxPerPage: 15
maxCounter: 10
canaryTokenTries: 10
probabilityErrorCodes: 0
# canaryTokenUrl: set-your-canary-token-url-here
networkPolicy:
enabled: true
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector: {}
- namespaceSelector: {}
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
port: 5000
egress:
- to:
- namespaceSelector: {}
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
- protocol: UDP
# Wordlists configuration
wordlists:
usernames:
prefixes:
- admin
- user
- developer
- root
- system
- db
- api
- service
- deploy
- test
- prod
- backup
- monitor
- jenkins
- webapp
suffixes:
- ""
- "_prod"
- "_dev"
- "_test"
- "123"
- "2024"
- "_backup"
- "_admin"
- "01"
- "02"
- "_user"
- "_service"
- "_api"
passwords:
prefixes:
- P@ssw0rd
- Passw0rd
- Admin
- Secret
- Welcome
- System
- Database
- Secure
- Master
- Root
simple:
- test
- demo
- temp
- change
- password
- admin
- letmein
- welcome
- default
- sample
emails:
domains:
- example.com
- company.com
- localhost.com
- test.com
- domain.com
- corporate.com
- internal.net
- enterprise.com
- business.org
api_keys:
prefixes:
- sk_live_
- sk_test_
- api_
- key_
- token_
- access_
- secret_
- prod_
- ""
databases:
names:
- production
- prod_db
- main_db
- app_database
- users_db
- customer_data
- analytics
- staging_db
- dev_database
- wordpress
- ecommerce
- crm_db
- inventory
hosts:
- localhost
- db.internal
- mysql.local
- postgres.internal
- 127.0.0.1
- db-server-01
- database.prod
- sql.company.com
applications:
names:
- WebApp
- API Gateway
- Dashboard
- Admin Panel
- CMS
- Portal
- Manager
- Console
- Control Panel
- Backend
users:
roles:
- Administrator
- Developer
- Manager
- User
- Guest
- Moderator
- Editor
- Viewer
- Analyst
- Support
directory_listing:
files:
- admin.txt
- test.exe
- backup.sql
- database.sql
- db_backup.sql
- dump.sql
- config.php
- credentials.txt
- passwords.txt
- users.csv
- .env
- id_rsa
- id_rsa.pub
- private_key.pem
- api_keys.json
- secrets.yaml
- admin_notes.txt
- settings.ini
- database.yml
- wp-config.php
- .htaccess
- server.key
- cert.pem
- shadow.bak
- passwd.old
directories:
- uploads/
- backups/
- logs/
- temp/
- cache/
- private/
- config/
- admin/
- database/
- backup/
- old/
- archive/
- .git/
- keys/
- credentials/
error_codes:
- 400
- 401
- 403
- 404
- 500
- 502
- 503

BIN
img/dashboard-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

BIN
img/dashboard-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
img/database.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
img/deception-page.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

16
manifests/configmap.yaml Normal file
View File

@@ -0,0 +1,16 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: krawl-config
namespace: krawl-system
data:
PORT: "5000"
DELAY: "100"
LINKS_MIN_LENGTH: "5"
LINKS_MAX_LENGTH: "15"
LINKS_MIN_PER_PAGE: "10"
LINKS_MAX_PER_PAGE: "15"
MAX_COUNTER: "10"
CANARY_TOKEN_TRIES: "10"
PROBABILITY_ERROR_CODES: "0"
# CANARY_TOKEN_URL: set-your-canary-token-url-here

60
manifests/deployment.yaml Normal file
View File

@@ -0,0 +1,60 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: krawl-server
namespace: krawl-system
labels:
app: krawl-server
spec:
replicas: 1
selector:
matchLabels:
app: krawl-server
template:
metadata:
labels:
app: krawl-server
spec:
containers:
- name: krawl
image: ghcr.io/blessedrebus/krawl:latest
imagePullPolicy: Always
ports:
- containerPort: 5000
name: http
protocol: TCP
envFrom:
- configMapRef:
name: krawl-config
volumeMounts:
- name: wordlists
mountPath: /app/wordlists.json
subPath: wordlists.json
readOnly: true
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /
port: 5000
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: 5000
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
volumes:
- name: wordlists
configMap:
name: krawl-wordlists

26
manifests/hpa.yaml Normal file
View File

@@ -0,0 +1,26 @@
# Optional: HorizontalPodAutoscaler for auto-scaling
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: krawl-hpa
namespace: krawl-system
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: krawl-server
minReplicas: 1
maxReplicas: 5
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80

24
manifests/ingress.yaml Normal file
View File

@@ -0,0 +1,24 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: krawl-ingress
namespace: krawl-system
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: krawl.example.com # Change to your domain
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: krawl-server
port:
number: 5000
# tls:
# - hosts:
# - krawl.example.com
# secretName: krawl-tls

View File

@@ -0,0 +1,14 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- configmap.yaml
- wordlists-configmap.yaml
- deployment.yaml
- service.yaml
- network-policy.yaml
- ingress.yaml
- hpa.yaml
namespace: krawl-system

4
manifests/namespace.yaml Normal file
View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: krawl-system

View File

@@ -0,0 +1,29 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: krawl-network-policy
namespace: krawl-system
spec:
podSelector:
matchLabels:
app: krawl-server
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector: {}
- namespaceSelector: {}
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
port: 5000
egress:
- to:
- namespaceSelector: {}
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
- protocol: UDP

16
manifests/service.yaml Normal file
View File

@@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: krawl-server
namespace: krawl-system
labels:
app: krawl-server
spec:
type: LoadBalancer
ports:
- port: 5000
targetPort: 5000
protocol: TCP
name: http
selector:
app: krawl-server

View File

@@ -0,0 +1,205 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: krawl-wordlists
namespace: krawl-system
data:
wordlists.json: |
{
"usernames": {
"prefixes": [
"admin",
"user",
"developer",
"root",
"system",
"db",
"api",
"service",
"deploy",
"test",
"prod",
"backup",
"monitor",
"jenkins",
"webapp"
],
"suffixes": [
"",
"_prod",
"_dev",
"_test",
"123",
"2024",
"_backup",
"_admin",
"01",
"02",
"_user",
"_service",
"_api"
]
},
"passwords": {
"prefixes": [
"P@ssw0rd",
"Passw0rd",
"Admin",
"Secret",
"Welcome",
"System",
"Database",
"Secure",
"Master",
"Root"
],
"simple": [
"test",
"demo",
"temp",
"change",
"password",
"admin",
"letmein",
"welcome",
"default",
"sample"
]
},
"emails": {
"domains": [
"example.com",
"company.com",
"localhost.com",
"test.com",
"domain.com",
"corporate.com",
"internal.net",
"enterprise.com",
"business.org"
]
},
"api_keys": {
"prefixes": [
"sk_live_",
"sk_test_",
"api_",
"key_",
"token_",
"access_",
"secret_",
"prod_",
""
]
},
"databases": {
"names": [
"production",
"prod_db",
"main_db",
"app_database",
"users_db",
"customer_data",
"analytics",
"staging_db",
"dev_database",
"wordpress",
"ecommerce",
"crm_db",
"inventory"
],
"hosts": [
"localhost",
"db.internal",
"mysql.local",
"postgres.internal",
"127.0.0.1",
"db-server-01",
"database.prod",
"sql.company.com"
]
},
"applications": {
"names": [
"WebApp",
"API Gateway",
"Dashboard",
"Admin Panel",
"CMS",
"Portal",
"Manager",
"Console",
"Control Panel",
"Backend"
]
},
"users": {
"roles": [
"Administrator",
"Developer",
"Manager",
"User",
"Guest",
"Moderator",
"Editor",
"Viewer",
"Analyst",
"Support"
]
},
"directory_listing": {
"files": [
"admin.txt",
"test.exe",
"backup.sql",
"database.sql",
"db_backup.sql",
"dump.sql",
"config.php",
"credentials.txt",
"passwords.txt",
"users.csv",
".env",
"id_rsa",
"id_rsa.pub",
"private_key.pem",
"api_keys.json",
"secrets.yaml",
"admin_notes.txt",
"settings.ini",
"database.yml",
"wp-config.php",
".htaccess",
"server.key",
"cert.pem",
"shadow.bak",
"passwd.old"
],
"directories": [
"uploads/",
"backups/",
"logs/",
"temp/",
"cache/",
"private/",
"config/",
"admin/",
"database/",
"backup/",
"old/",
"archive/",
".git/",
"keys/",
"credentials/"
]
},
"error_codes": [
400,
401,
403,
404,
500,
502,
503
]
}

48
src/config.py Normal file
View File

@@ -0,0 +1,48 @@
#!/usr/bin/env python3
import os
from dataclasses import dataclass
from typing import Optional, Tuple
@dataclass
class Config:
"""Configuration class for the deception server"""
port: int = 5000
delay: int = 100 # milliseconds
links_length_range: Tuple[int, int] = (5, 15)
links_per_page_range: Tuple[int, int] = (10, 15)
char_space: str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
max_counter: int = 10
canary_token_url: Optional[str] = None
canary_token_tries: int = 10
dashboard_secret_path: str = None
api_server_url: Optional[str] = None
api_server_port: int = 8080
api_server_path: str = "/api/v2/users"
probability_error_codes: int = 0 # Percentage (0-100)
@classmethod
def from_env(cls) -> 'Config':
"""Create configuration from environment variables"""
return cls(
port=int(os.getenv('PORT', 5000)),
delay=int(os.getenv('DELAY', 100)),
links_length_range=(
int(os.getenv('LINKS_MIN_LENGTH', 5)),
int(os.getenv('LINKS_MAX_LENGTH', 15))
),
links_per_page_range=(
int(os.getenv('LINKS_MIN_PER_PAGE', 10)),
int(os.getenv('LINKS_MAX_PER_PAGE', 15))
),
char_space=os.getenv('CHAR_SPACE', 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'),
max_counter=int(os.getenv('MAX_COUNTER', 10)),
canary_token_url=os.getenv('CANARY_TOKEN_URL'),
canary_token_tries=int(os.getenv('CANARY_TOKEN_TRIES', 10)),
dashboard_secret_path=os.getenv('DASHBOARD_SECRET_PATH', f'/{os.urandom(16).hex()}'),
api_server_url=os.getenv('API_SERVER_URL'),
api_server_port=int(os.getenv('API_SERVER_PORT', 8080)),
api_server_path=os.getenv('API_SERVER_PATH', '/api/v2/users'),
probability_error_codes=int(os.getenv('PROBABILITY_ERROR_CODES', 5))
)

214
src/dashboard_template.py Normal file
View File

@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""
Dashboard template for viewing honeypot statistics.
Customize this template to change the dashboard appearance.
"""
def generate_dashboard(stats: dict) -> str:
"""Generate dashboard HTML with access statistics"""
top_ips_rows = '\n'.join([
f'<tr><td class="rank">{i+1}</td><td>{ip}</td><td>{count}</td></tr>'
for i, (ip, count) in enumerate(stats['top_ips'])
]) or '<tr><td colspan="3" style="text-align:center;">No data</td></tr>'
# Generate paths rows
top_paths_rows = '\n'.join([
f'<tr><td class="rank">{i+1}</td><td>{path}</td><td>{count}</td></tr>'
for i, (path, count) in enumerate(stats['top_paths'])
]) or '<tr><td colspan="3" style="text-align:center;">No data</td></tr>'
# Generate User-Agent rows
top_ua_rows = '\n'.join([
f'<tr><td class="rank">{i+1}</td><td style="word-break: break-all;">{ua[:80]}</td><td>{count}</td></tr>'
for i, (ua, count) in enumerate(stats['top_user_agents'])
]) or '<tr><td colspan="3" style="text-align:center;">No data</td></tr>'
# Generate suspicious accesses rows
suspicious_rows = '\n'.join([
f'<tr><td>{log["ip"]}</td><td>{log["path"]}</td><td style="word-break: break-all;">{log["user_agent"][:60]}</td><td>{log["timestamp"].split("T")[1][:8]}</td></tr>'
for log in stats['recent_suspicious'][-10:]
]) or '<tr><td colspan="4" style="text-align:center;">No suspicious activity detected</td></tr>'
return f"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Krawl Dashboard</title>
<style>
body {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #0d1117;
color: #c9d1d9;
margin: 0;
padding: 20px;
}}
.container {{
max-width: 1400px;
margin: 0 auto;
}}
h1 {{
color: #58a6ff;
text-align: center;
margin-bottom: 40px;
}}
.stats-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 40px;
}}
.stat-card {{
background: #161b22;
border: 1px solid #30363d;
border-radius: 6px;
padding: 20px;
text-align: center;
}}
.stat-card.alert {{
border-color: #f85149;
}}
.stat-value {{
font-size: 36px;
font-weight: bold;
color: #58a6ff;
}}
.stat-value.alert {{
color: #f85149;
}}
.stat-label {{
font-size: 14px;
color: #8b949e;
margin-top: 5px;
}}
.table-container {{
background: #161b22;
border: 1px solid #30363d;
border-radius: 6px;
padding: 20px;
margin-bottom: 20px;
}}
h2 {{
color: #58a6ff;
margin-top: 0;
}}
table {{
width: 100%;
border-collapse: collapse;
}}
th, td {{
padding: 12px;
text-align: left;
border-bottom: 1px solid #30363d;
}}
th {{
background: #0d1117;
color: #58a6ff;
font-weight: 600;
}}
tr:hover {{
background: #1c2128;
}}
.rank {{
color: #8b949e;
font-weight: bold;
}}
.alert-section {{
background: #1c1917;
border-left: 4px solid #f85149;
}}
</style>
</head>
<body>
<div class="container">
<h1>&#128375;&#65039; Krawl Dashboard</h1>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">{stats['total_accesses']}</div>
<div class="stat-label">Total Accesses</div>
</div>
<div class="stat-card">
<div class="stat-value">{stats['unique_ips']}</div>
<div class="stat-label">Unique IPs</div>
</div>
<div class="stat-card">
<div class="stat-value">{stats['unique_paths']}</div>
<div class="stat-label">Unique Paths</div>
</div>
<div class="stat-card alert">
<div class="stat-value alert">{stats['suspicious_accesses']}</div>
<div class="stat-label">Suspicious Accesses</div>
</div>
</div>
<div class="table-container alert-section">
<h2>&#9888;&#65039; Recent Suspicious Activity</h2>
<table>
<thead>
<tr>
<th>IP Address</th>
<th>Path</th>
<th>User-Agent</th>
<th>Time</th>
</tr>
</thead>
<tbody>
{suspicious_rows}
</tbody>
</table>
</div>
<div class="table-container">
<h2>Top IP Addresses</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>IP Address</th>
<th>Access Count</th>
</tr>
</thead>
<tbody>
{top_ips_rows}
</tbody>
</table>
</div>
<div class="table-container">
<h2>Top Paths</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>Path</th>
<th>Access Count</th>
</tr>
</thead>
<tbody>
{top_paths_rows}
</tbody>
</table>
</div>
<div class="table-container">
<h2>Top User-Agents</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>User-Agent</th>
<th>Count</th>
</tr>
</thead>
<tbody>
{top_ua_rows}
</tbody>
</table>
</div>
</div>
</body>
</html>
"""

190
src/generators.py Normal file
View File

@@ -0,0 +1,190 @@
#!/usr/bin/env python3
"""
Generators for creating random fake data (credentials, API keys, etc.)
"""
import random
import string
import json
from templates import html_templates
from wordlists import get_wordlists
def random_username() -> str:
"""Generate random username"""
wl = get_wordlists()
return random.choice(wl.username_prefixes) + random.choice(wl.username_suffixes)
def random_password() -> str:
"""Generate random password"""
wl = get_wordlists()
templates = [
lambda: ''.join(random.choices(string.ascii_letters + string.digits, k=12)),
lambda: f"{random.choice(wl.password_prefixes)}{random.randint(100, 999)}!",
lambda: f"{random.choice(wl.simple_passwords)}{random.randint(1000, 9999)}",
lambda: ''.join(random.choices(string.ascii_lowercase, k=8)),
]
return random.choice(templates)()
def random_email(username: str = None) -> str:
"""Generate random email"""
wl = get_wordlists()
if not username:
username = random_username()
return f"{username}@{random.choice(wl.email_domains)}"
def random_api_key() -> str:
"""Generate random API key"""
wl = get_wordlists()
key = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
return random.choice(wl.api_key_prefixes) + key
def random_database_name() -> str:
"""Generate random database name"""
wl = get_wordlists()
return random.choice(wl.database_names)
def credentials_txt() -> str:
"""Generate fake credentials.txt with random data"""
content = "# Production Credentials\n\n"
for i in range(random.randint(3, 7)):
username = random_username()
password = random_password()
content += f"{username}:{password}\n"
return content
def passwords_txt() -> str:
"""Generate fake passwords.txt with random data"""
content = "# Password List\n"
content += f"Admin Password: {random_password()}\n"
content += f"Database Password: {random_password()}\n"
content += f"API Key: {random_api_key()}\n\n"
content += "User Passwords:\n"
for i in range(random.randint(5, 10)):
username = random_username()
password = random_password()
content += f"{username} = {password}\n"
return content
def users_json() -> str:
"""Generate fake users.json with random data"""
wl = get_wordlists()
users = []
for i in range(random.randint(3, 8)):
username = random_username()
users.append({
"id": i + 1,
"username": username,
"email": random_email(username),
"password": random_password(),
"role": random.choice(wl.user_roles),
"api_token": random_api_key()
})
return json.dumps({"users": users}, indent=2)
def api_keys_json() -> str:
"""Generate fake api_keys.json with random data"""
keys = {
"stripe": {
"public_key": "pk_live_" + ''.join(random.choices(string.ascii_letters + string.digits, k=24)),
"secret_key": random_api_key()
},
"aws": {
"access_key_id": "AKIA" + ''.join(random.choices(string.ascii_uppercase + string.digits, k=16)),
"secret_access_key": ''.join(random.choices(string.ascii_letters + string.digits + '+/', k=40))
},
"sendgrid": {
"api_key": "SG." + ''.join(random.choices(string.ascii_letters + string.digits, k=48))
},
"twilio": {
"account_sid": "AC" + ''.join(random.choices(string.ascii_lowercase + string.digits, k=32)),
"auth_token": ''.join(random.choices(string.ascii_lowercase + string.digits, k=32))
}
}
return json.dumps(keys, indent=2)
def api_response(path: str) -> str:
"""Generate fake API JSON responses with random data"""
wl = get_wordlists()
def random_users(count: int = 3):
users = []
for i in range(count):
username = random_username()
users.append({
"id": i + 1,
"username": username,
"email": random_email(username),
"role": random.choice(wl.user_roles)
})
return users
responses = {
'/api/users': json.dumps({
"users": random_users(random.randint(2, 5)),
"total": random.randint(50, 500)
}, indent=2),
'/api/v1/users': json.dumps({
"status": "success",
"data": [{
"id": random.randint(1, 100),
"name": random_username(),
"api_key": random_api_key()
}]
}, indent=2),
'/api/v2/secrets': json.dumps({
"database": {
"host": random.choice(wl.database_hosts),
"username": random_username(),
"password": random_password(),
"database": random_database_name()
},
"api_keys": {
"stripe": random_api_key(),
"aws": 'AKIA' + ''.join(random.choices(string.ascii_uppercase + string.digits, k=16))
}
}, indent=2),
'/api/config': json.dumps({
"app_name": random.choice(wl.application_names),
"debug": random.choice([True, False]),
"secret_key": random_api_key(),
"database_url": f"postgresql://{random_username()}:{random_password()}@localhost/{random_database_name()}"
}, indent=2),
'/.env': f"""APP_NAME={random.choice(wl.application_names)}
DEBUG={random.choice(['true', 'false'])}
APP_KEY=base64:{''.join(random.choices(string.ascii_letters + string.digits, k=32))}=
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE={random_database_name()}
DB_USERNAME={random_username()}
DB_PASSWORD={random_password()}
AWS_ACCESS_KEY_ID=AKIA{''.join(random.choices(string.ascii_uppercase + string.digits, k=16))}
AWS_SECRET_ACCESS_KEY={''.join(random.choices(string.ascii_letters + string.digits + '+/', k=40))}
STRIPE_SECRET={random_api_key()}
"""
}
return responses.get(path, json.dumps({"error": "Not found"}, indent=2))
def directory_listing(path: str) -> str:
"""Generate fake directory listing using wordlists"""
wl = get_wordlists()
files = wl.directory_files
dirs = wl.directory_dirs
selected_files = [(f, random.randint(1024, 1024*1024))
for f in random.sample(files, min(6, len(files)))]
return html_templates.directory_listing(path, dirs, selected_files)

342
src/handler.py Normal file
View File

@@ -0,0 +1,342 @@
#!/usr/bin/env python3
import random
import time
from http.server import BaseHTTPRequestHandler
from typing import Optional, List
from datetime import datetime
from config import Config
from tracker import AccessTracker
from templates import html_templates
from templates.dashboard_template import generate_dashboard
from generators import (
credentials_txt, passwords_txt, users_json, api_keys_json,
api_response, directory_listing
)
from wordlists import get_wordlists
class Handler(BaseHTTPRequestHandler):
"""HTTP request handler for the deception server"""
webpages: Optional[List[str]] = None
config: Config = None
tracker: AccessTracker = None
counter: int = 0
def _get_client_ip(self) -> str:
"""Extract client IP address from request, checking proxy headers first"""
# Headers might not be available during early error logging
if hasattr(self, 'headers') and self.headers:
# Check X-Forwarded-For header (set by load balancers/proxies)
forwarded_for = self.headers.get('X-Forwarded-For')
if forwarded_for:
# X-Forwarded-For can contain multiple IPs, get the first (original client)
return forwarded_for.split(',')[0].strip()
# Check X-Real-IP header (set by nginx and other proxies)
real_ip = self.headers.get('X-Real-IP')
if real_ip:
return real_ip.strip()
# Fallback to direct connection IP
return self.client_address[0]
def _get_user_agent(self) -> str:
"""Extract user agent from request"""
return self.headers.get('User-Agent', '')
def _should_return_error(self) -> bool:
"""Check if we should return an error based on probability"""
if self.config.probability_error_codes <= 0:
return False
return random.randint(1, 100) <= self.config.probability_error_codes
def _get_random_error_code(self) -> int:
"""Get a random error code from wordlists"""
wl = get_wordlists()
error_codes = wl.error_codes
if not error_codes:
error_codes = [400, 401, 403, 404, 500, 502, 503]
return random.choice(error_codes)
def generate_page(self, seed: str) -> str:
"""Generate a webpage containing random links or canary token"""
random.seed(seed)
num_pages = random.randint(*self.config.links_per_page_range)
html = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Krawl</title>
<style>
body {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #0d1117;
color: #c9d1d9;
margin: 0;
padding: 40px 20px;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}}
.container {{
max-width: 1200px;
width: 100%;
}}
h1 {{
color: #f85149;
text-align: center;
font-size: 48px;
margin: 60px 0 30px;
}}
.counter {{
color: #f85149;
text-align: center;
font-size: 56px;
font-weight: bold;
margin-bottom: 60px;
}}
.links-container {{
display: flex;
flex-direction: column;
gap: 20px;
align-items: center;
}}
.link-box {{
background: #161b22;
border: 1px solid #30363d;
border-radius: 6px;
padding: 15px 30px;
min-width: 300px;
text-align: center;
transition: all 0.3s ease;
}}
.link-box:hover {{
background: #1c2128;
border-color: #58a6ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(88, 166, 255, 0.2);
}}
a {{
color: #58a6ff;
text-decoration: none;
font-size: 20px;
font-weight: 700;
}}
a:hover {{
color: #79c0ff;
}}
.canary-token {{
background: #1c1917;
border: 2px solid #f85149;
border-radius: 8px;
padding: 30px 50px;
margin: 40px auto;
max-width: 800px;
overflow-x: auto;
}}
.canary-token a {{
color: #f85149;
font-size: 18px;
white-space: nowrap;
}}
</style>
</head>
<body>
<div class="container">
<h1>Krawl me! &#128376;</h1>
<div class="counter">{Handler.counter}</div>
<div class="links-container">
"""
if Handler.counter <= 0 and self.config.canary_token_url:
html += f"""
<div class="link-box canary-token">
<a href="{self.config.canary_token_url}">{self.config.canary_token_url}</a>
</div>
"""
if self.webpages is None:
for _ in range(num_pages):
address = ''.join([
random.choice(self.config.char_space)
for _ in range(random.randint(*self.config.links_length_range))
])
html += f"""
<div class="link-box">
<a href="{address}">{address}</a>
</div>
"""
else:
for _ in range(num_pages):
address = random.choice(self.webpages)
html += f"""
<div class="link-box">
<a href="{address}">{address}</a>
</div>
"""
html += """
</div>
</div>
</body>
</html>"""
return html
def do_HEAD(self):
"""Sends header information"""
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
def do_POST(self):
"""Handle POST requests (mainly login attempts)"""
client_ip = self._get_client_ip()
user_agent = self._get_user_agent()
self.tracker.record_access(client_ip, self.path, user_agent)
print(f"[LOGIN ATTEMPT] {client_ip} - {self.path} - {user_agent[:50]}")
content_length = int(self.headers.get('Content-Length', 0))
if content_length > 0:
post_data = self.rfile.read(content_length).decode('utf-8')
print(f"[POST DATA] {post_data[:200]}")
time.sleep(1)
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html_templates.login_error().encode())
def serve_special_path(self, path: str) -> bool:
"""Serve special paths like robots.txt, API endpoints, etc."""
if path == '/robots.txt':
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write(html_templates.robots_txt().encode())
return True
if path in ['/credentials.txt', '/passwords.txt', '/admin_notes.txt']:
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
if 'credentials' in path:
self.wfile.write(credentials_txt().encode())
else:
self.wfile.write(passwords_txt().encode())
return True
if path in ['/users.json', '/api_keys.json', '/config.json']:
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
if 'users' in path:
self.wfile.write(users_json().encode())
elif 'api_keys' in path:
self.wfile.write(api_keys_json().encode())
else:
self.wfile.write(api_response('/api/config').encode())
return True
if path in ['/admin', '/admin/', '/admin/login', '/login', '/wp-login.php']:
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html_templates.login_form().encode())
return True
if path == '/wp-admin' or path == '/wp-admin/':
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html_templates.login_form().encode())
return True
if path in ['/wp-content/', '/wp-includes/'] or 'wordpress' in path.lower():
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html_templates.wordpress().encode())
return True
if 'phpmyadmin' in path.lower() or path in ['/pma/', '/phpMyAdmin/']:
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html_templates.phpmyadmin().encode())
return True
if path.startswith('/api/') or path.startswith('/api') or path in ['/.env']:
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(api_response(path).encode())
return True
if path in ['/backup/', '/uploads/', '/private/', '/admin/', '/config/', '/database/']:
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(directory_listing(path).encode())
return True
return False
def do_GET(self):
"""Responds to webpage requests"""
client_ip = self._get_client_ip()
user_agent = self._get_user_agent()
if self.config.dashboard_secret_path and self.path == self.config.dashboard_secret_path:
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
try:
stats = self.tracker.get_stats()
self.wfile.write(generate_dashboard(stats).encode())
except Exception as e:
print(f"Error generating dashboard: {e}")
return
self.tracker.record_access(client_ip, self.path, user_agent)
if self.tracker.is_suspicious_user_agent(user_agent):
print(f"[SUSPICIOUS] {client_ip} - {user_agent[:50]} - {self.path}")
if self._should_return_error():
error_code = self._get_random_error_code()
print(f"[ERROR] Returning {error_code} to {client_ip} - {self.path}")
self.send_response(error_code)
self.end_headers()
return
if self.serve_special_path(self.path):
return
time.sleep(self.config.delay / 1000.0)
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
try:
self.wfile.write(self.generate_page(self.path).encode())
Handler.counter -= 1
if Handler.counter < 0:
Handler.counter = self.config.canary_token_tries
except Exception as e:
print(f"Error generating page: {e}")
def log_message(self, format, *args):
"""Override to customize logging"""
client_ip = self._get_client_ip()
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {client_ip} - {format % args}")

83
src/server.py Normal file
View File

@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""
Main server module for the deception honeypot.
Run this file to start the server.
"""
import sys
from http.server import HTTPServer
from config import Config
from tracker import AccessTracker
from handler import Handler
def print_usage():
"""Print usage information"""
print(f'Usage: {sys.argv[0]} [FILE]\n')
print('FILE is file containing a list of webpage names to serve, one per line.')
print('If no file is provided, random links will be generated.\n')
print('Environment Variables:')
print(' PORT - Server port (default: 5000)')
print(' DELAY - Response delay in ms (default: 100)')
print(' LINKS_MIN_LENGTH - Min link length (default: 5)')
print(' LINKS_MAX_LENGTH - Max link length (default: 15)')
print(' LINKS_MIN_PER_PAGE - Min links per page (default: 10)')
print(' LINKS_MAX_PER_PAGE - Max links per page (default: 15)')
print(' MAX_COUNTER - Max counter value (default: 10)')
print(' CANARY_TOKEN_URL - Canary token URL to display')
print(' CANARY_TOKEN_TRIES - Number of tries before showing token (default: 10)')
print(' DASHBOARD_SECRET_PATH - Secret path for dashboard (auto-generated if not set)')
print(' PROBABILITY_ERROR_CODES - Probability (0-100) to return HTTP error codes (default: 0)')
print(' CHAR_SPACE - Characters for random links')
def main():
"""Main entry point for the deception server"""
if '-h' in sys.argv or '--help' in sys.argv:
print_usage()
exit(0)
config = Config.from_env()
tracker = AccessTracker()
Handler.config = config
Handler.tracker = tracker
Handler.counter = config.canary_token_tries
if len(sys.argv) == 2:
try:
with open(sys.argv[1], 'r') as f:
Handler.webpages = f.readlines()
if not Handler.webpages:
print('The file provided was empty. Using randomly generated links.')
Handler.webpages = None
except IOError:
print('Can\'t read input file. Using randomly generated links.')
try:
print(f'Starting deception server on port {config.port}...')
print(f'Dashboard available at: {config.dashboard_secret_path}')
if config.canary_token_url:
print(f'Canary token will appear after {config.canary_token_tries} tries')
else:
print('No canary token configured (set CANARY_TOKEN_URL to enable)')
server = HTTPServer(('0.0.0.0', config.port), Handler)
print('Server started. Use <Ctrl-C> to stop.')
server.serve_forever()
except KeyboardInterrupt:
print('\nStopping server...')
server.socket.close()
print('Server stopped')
except Exception as e:
print(f'Error starting HTTP server on port {config.port}: {e}')
print(f'Make sure you are root, if needed, and that port {config.port} is open.')
exit(1)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,241 @@
#!/usr/bin/env python3
"""
Dashboard template for viewing honeypot statistics.
Customize this template to change the dashboard appearance.
"""
def generate_dashboard(stats: dict) -> str:
"""Generate dashboard HTML with access statistics"""
# Generate IP rows
top_ips_rows = '\n'.join([
f'<tr><td class="rank">{i+1}</td><td>{ip}</td><td>{count}</td></tr>'
for i, (ip, count) in enumerate(stats['top_ips'])
]) or '<tr><td colspan="3" style="text-align:center;">No data</td></tr>'
# Generate paths rows
top_paths_rows = '\n'.join([
f'<tr><td class="rank">{i+1}</td><td>{path}</td><td>{count}</td></tr>'
for i, (path, count) in enumerate(stats['top_paths'])
]) or '<tr><td colspan="3" style="text-align:center;">No data</td></tr>'
# Generate User-Agent rows
top_ua_rows = '\n'.join([
f'<tr><td class="rank">{i+1}</td><td style="word-break: break-all;">{ua[:80]}</td><td>{count}</td></tr>'
for i, (ua, count) in enumerate(stats['top_user_agents'])
]) or '<tr><td colspan="3" style="text-align:center;">No data</td></tr>'
# Generate suspicious accesses rows
suspicious_rows = '\n'.join([
f'<tr><td>{log["ip"]}</td><td>{log["path"]}</td><td style="word-break: break-all;">{log["user_agent"][:60]}</td><td>{log["timestamp"].split("T")[1][:8]}</td></tr>'
for log in stats['recent_suspicious'][-10:]
]) or '<tr><td colspan="4" style="text-align:center;">No suspicious activity detected</td></tr>'
# Generate honeypot triggered IPs rows
honeypot_rows = '\n'.join([
f'<tr><td>{ip}</td><td style="word-break: break-all;">{", ".join(paths)}</td><td>{len(paths)}</td></tr>'
for ip, paths in stats.get('honeypot_triggered_ips', [])
]) or '<tr><td colspan="3" style="text-align:center;">No honeypot triggers yet</td></tr>'
return f"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Krawl Dashboard</title>
<style>
body {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #0d1117;
color: #c9d1d9;
margin: 0;
padding: 20px;
}}
.container {{
max-width: 1400px;
margin: 0 auto;
}}
h1 {{
color: #58a6ff;
text-align: center;
margin-bottom: 40px;
}}
.stats-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 40px;
}}
.stat-card {{
background: #161b22;
border: 1px solid #30363d;
border-radius: 6px;
padding: 20px;
text-align: center;
}}
.stat-card.alert {{
border-color: #f85149;
}}
.stat-value {{
font-size: 36px;
font-weight: bold;
color: #58a6ff;
}}
.stat-value.alert {{
color: #f85149;
}}
.stat-label {{
font-size: 14px;
color: #8b949e;
margin-top: 5px;
}}
.table-container {{
background: #161b22;
border: 1px solid #30363d;
border-radius: 6px;
padding: 20px;
margin-bottom: 20px;
}}
h2 {{
color: #58a6ff;
margin-top: 0;
}}
table {{
width: 100%;
border-collapse: collapse;
}}
th, td {{
padding: 12px;
text-align: left;
border-bottom: 1px solid #30363d;
}}
th {{
background: #0d1117;
color: #58a6ff;
font-weight: 600;
}}
tr:hover {{
background: #1c2128;
}}
.rank {{
color: #8b949e;
font-weight: bold;
}}
.alert-section {{
background: #1c1917;
border-left: 4px solid #f85149;
}}
</style>
</head>
<body>
<div class="container">
<h1>&#128375;&#65039; Krawl Dashboard</h1>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">{stats['total_accesses']}</div>
<div class="stat-label">Total Accesses</div>
</div>
<div class="stat-card">
<div class="stat-value">{stats['unique_ips']}</div>
<div class="stat-label">Unique IPs</div>
</div>
<div class="stat-card">
<div class="stat-value">{stats['unique_paths']}</div>
<div class="stat-label">Unique Paths</div>
</div>
<div class="stat-card alert">
<div class="stat-value alert">{stats['suspicious_accesses']}</div>
<div class="stat-label">Suspicious Accesses</div>
</div>
<div class="stat-card alert">
<div class="stat-value alert">{stats.get('honeypot_ips', 0)}</div>
<div class="stat-label">Honeypot Caught</div>
</div>
</div>
<div class="table-container alert-section">
<h2>🍯 Honeypot Triggers</h2>
<table>
<thead>
<tr>
<th>IP Address</th>
<th>Accessed Paths</th>
<th>Count</th>
</tr>
</thead>
<tbody>
{honeypot_rows}
</tbody>
</table>
</div>
<div class="table-container alert-section">
<h2>&#9888;&#65039; Recent Suspicious Activity</h2>
<table>
<thead>
<tr>
<th>IP Address</th>
<th>Path</th>
<th>User-Agent</th>
<th>Time</th>
</tr>
</thead>
<tbody>
{suspicious_rows}
</tbody>
</table>
</div>
<div class="table-container">
<h2>Top IP Addresses</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>IP Address</th>
<th>Access Count</th>
</tr>
</thead>
<tbody>
{top_ips_rows}
</tbody>
</table>
</div>
<div class="table-container">
<h2>Top Paths</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>Path</th>
<th>Access Count</th>
</tr>
</thead>
<tbody>
{top_paths_rows}
</tbody>
</table>
</div>
<div class="table-container">
<h2>Top User-Agents</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>User-Agent</th>
<th>Count</th>
</tr>
</thead>
<tbody>
{top_ua_rows}
</tbody>
</table>
</div>
</div>
</body>
</html>
"""

View File

@@ -0,0 +1,229 @@
#!/usr/bin/env python3
"""
HTML templates for the deception server.
Edit these templates to customize the appearance of fake pages.
"""
def login_form() -> str:
"""Generate fake login page"""
return """<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Admin Login</title>
<style>
body { font-family: Arial, sans-serif; background: #f0f0f0; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
.login-box { background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); width: 300px; }
h2 { margin-top: 0; color: #333; }
input { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
button { width: 100%; padding: 10px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #0056b3; }
</style>
</head>
<body>
<div class="login-box">
<h2>Admin Login</h2>
<form action="/admin/login" method="post">
<input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Login</button>
</form>
</div>
</body>
</html>"""
def login_error() -> str:
"""Generate fake login error page"""
return """<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Login Failed</title>
<style>
body { font-family: Arial, sans-serif; background: #f0f0f0; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
.login-box { background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); width: 300px; }
h2 { margin-top: 0; color: #333; }
.error { color: #d63301; background: #ffebe8; border: 1px solid #d63301; padding: 12px; margin-bottom: 20px; border-radius: 4px; }
input { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
button { width: 100%; padding: 10px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #0056b3; }
a { color: #007bff; font-size: 14px; }
</style>
</head>
<body>
<div class="login-box">
<h2>Admin Login</h2>
<div class="error"><strong>ERROR:</strong> Invalid username or password.</div>
<form action="/admin/login" method="post">
<input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Login</button>
</form>
<p style="margin-top: 20px; text-align: center;"><a href="/forgot-password">Forgot your password?</a></p>
</div>
</body>
</html>"""
def wordpress() -> str:
"""Generate fake WordPress page"""
return """<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My Blog &#8211; Just another WordPress site</title>
<link rel='dns-prefetch' href='//s.w.org' />
<link rel='stylesheet' id='wp-block-library-css' href='/wp-includes/css/dist/block-library/style.min.css' type='text/css' media='all' />
<link rel='stylesheet' id='twentytwentythree-style-css' href='/wp-content/themes/twentytwentythree/style.css' type='text/css' media='all' />
<link rel='https://api.w.org/' href='/wp-json/' />
<meta name="generator" content="WordPress 6.4.2" />
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; margin: 0; padding: 0; background: #fff; }
.site-header { background: #23282d; color: white; padding: 20px; border-bottom: 4px solid #0073aa; }
.site-header h1 { margin: 0; font-size: 28px; }
.site-header p { margin: 5px 0 0; color: #d0d0d0; }
.site-content { max-width: 1200px; margin: 40px auto; padding: 0 20px; }
.entry { background: #fff; margin-bottom: 40px; padding: 30px; border: 1px solid #ddd; border-radius: 4px; }
.entry-title { font-size: 32px; margin-top: 0; color: #23282d; }
.entry-meta { color: #666; font-size: 14px; margin-bottom: 20px; }
.entry-content { line-height: 1.8; color: #444; }
.site-footer { background: #f7f7f7; padding: 20px; text-align: center; color: #666; border-top: 1px solid #ddd; margin-top: 60px; }
a { color: #0073aa; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body class="home blog wp-embed-responsive">
<div id="page" class="site">
<header id="masthead" class="site-header">
<div class="site-branding">
<h1 class="site-title">My Blog</h1>
<p class="site-description">Just another WordPress site</p>
</div>
</header>
<div id="content" class="site-content">
<article id="post-1" class="entry">
<header class="entry-header">
<h2 class="entry-title">Hello world!</h2>
<div class="entry-meta">
<span class="posted-on">Posted on <time datetime="2024-12-01">December 1, 2024</time></span>
<span class="byline"> by <span class="author">admin</span></span>
</div>
</header>
<div class="entry-content">
<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
</div>
</article>
<article id="post-2" class="entry">
<header class="entry-header">
<h2 class="entry-title">About This Site</h2>
<div class="entry-meta">
<span class="posted-on">Posted on <time datetime="2024-11-28">November 28, 2024</time></span>
<span class="byline"> by <span class="author">admin</span></span>
</div>
</header>
<div class="entry-content">
<p>This is a sample page. You can use it to write about your site, yourself, or anything else you'd like.</p>
</div>
</article>
</div>
<footer id="colophon" class="site-footer">
<div class="site-info">
Proudly powered by <a href="https://wordpress.org/">WordPress</a>
</div>
</footer>
</div>
<script type='text/javascript' src='/wp-includes/js/wp-embed.min.js'></script>
</body>
</html>"""
def phpmyadmin() -> str:
"""Generate fake phpMyAdmin page"""
return """<!DOCTYPE html>
<html>
<head>
<title>phpMyAdmin</title>
<style>
body { font-family: 'Segoe UI', Tahoma, sans-serif; margin: 0; background: #f0f0f0; }
.header { background: #2979ff; color: white; padding: 10px 20px; }
.login { background: white; width: 400px; margin: 100px auto; padding: 30px; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
input { width: 100%; padding: 8px; margin: 8px 0; border: 1px solid #ddd; }
button { padding: 10px 20px; background: #2979ff; color: white; border: none; cursor: pointer; }
</style>
</head>
<body>
<div class="header"><h1>phpMyAdmin</h1></div>
<div class="login">
<h2>MySQL Server Login</h2>
<form action="/phpMyAdmin/index.php" method="post">
<input type="text" name="pma_username" placeholder="Username">
<input type="password" name="pma_password" placeholder="Password">
<button type="submit">Go</button>
</form>
</div>
</body>
</html>"""
def robots_txt() -> str:
"""Generate juicy robots.txt"""
return """User-agent: *
Disallow: /admin/
Disallow: /api/
Disallow: /backup/
Disallow: /config/
Disallow: /database/
Disallow: /private/
Disallow: /uploads/
Disallow: /wp-admin/
Disallow: /phpMyAdmin/
Disallow: /admin/login.php
Disallow: /api/v1/users
Disallow: /api/v2/secrets
Disallow: /.env
Disallow: /credentials.txt
Disallow: /passwords.txt
Disallow: /.git/
Disallow: /backup.sql
Disallow: /db_backup.sql
"""
def directory_listing(path: str, dirs: list, files: list) -> str:
"""Generate fake directory listing"""
html = f"""<!DOCTYPE html>
<html>
<head><title>Index of {path}</title>
<style>
body {{ font-family: monospace; background: #fff; padding: 20px; }}
h1 {{ border-bottom: 1px solid #ccc; padding-bottom: 10px; }}
table {{ width: 100%; border-collapse: collapse; }}
th {{ text-align: left; padding: 10px; background: #f0f0f0; }}
td {{ padding: 8px; border-bottom: 1px solid #eee; }}
a {{ color: #0066cc; text-decoration: none; }}
a:hover {{ text-decoration: underline; }}
</style>
</head>
<body>
<h1>Index of {path}</h1>
<table>
<tr><th>Name</th><th>Last Modified</th><th>Size</th></tr>
<tr><td><a href="../">Parent Directory</a></td><td>-</td><td>-</td></tr>
"""
for d in dirs:
html += f'<tr><td><a href="{d}">{d}</a></td><td>2024-12-01 10:30</td><td>-</td></tr>\n'
for f, size in files:
html += f'<tr><td><a href="{f}">{f}</a></td><td>2024-12-01 14:22</td><td>{size}</td></tr>\n'
html += '</table></body></html>'
return html

114
src/tracker.py Normal file
View File

@@ -0,0 +1,114 @@
#!/usr/bin/env python3
from typing import Dict, List, Tuple
from collections import defaultdict
from datetime import datetime
class AccessTracker:
"""Track IP addresses and paths accessed"""
def __init__(self):
self.ip_counts: Dict[str, int] = defaultdict(int)
self.path_counts: Dict[str, int] = defaultdict(int)
self.user_agent_counts: Dict[str, int] = defaultdict(int)
self.access_log: List[Dict] = []
self.suspicious_patterns = [
'bot', 'crawler', 'spider', 'scraper', 'curl', 'wget', 'python-requests',
'scanner', 'nikto', 'sqlmap', 'nmap', 'masscan', 'nessus', 'acunetix',
'burp', 'zap', 'w3af', 'metasploit', 'nuclei', 'gobuster', 'dirbuster'
]
# Track IPs that accessed honeypot paths from robots.txt
self.honeypot_triggered: Dict[str, List[str]] = defaultdict(list)
def record_access(self, ip: str, path: str, user_agent: str = ''):
"""Record an access attempt"""
self.ip_counts[ip] += 1
self.path_counts[path] += 1
if user_agent:
self.user_agent_counts[user_agent] += 1
is_suspicious = self.is_suspicious_user_agent(user_agent) or self.is_honeypot_path(path)
# Track if this IP accessed a honeypot path
if self.is_honeypot_path(path):
self.honeypot_triggered[ip].append(path)
self.access_log.append({
'ip': ip,
'path': path,
'user_agent': user_agent,
'suspicious': is_suspicious,
'honeypot_triggered': self.is_honeypot_path(path),
'timestamp': datetime.now().isoformat()
})
def is_honeypot_path(self, path: str) -> bool:
"""Check if path is one of the honeypot traps from robots.txt"""
honeypot_paths = [
'/admin',
'/admin/',
'/backup',
'/backup/',
'/config',
'/config/',
'/private',
'/private/',
'/database',
'/database/',
'/credentials.txt',
'/passwords.txt',
'/admin_notes.txt',
'/api_keys.json',
'/.env',
'/wp-admin',
'/wp-admin/',
'/phpmyadmin',
'/phpMyAdmin/'
]
return path in honeypot_paths or any(hp in path.lower() for hp in ['/backup', '/admin', '/config', '/private', '/database', 'phpmyadmin'])
def is_suspicious_user_agent(self, user_agent: str) -> bool:
"""Check if user agent matches suspicious patterns"""
if not user_agent:
return True
ua_lower = user_agent.lower()
return any(pattern in ua_lower for pattern in self.suspicious_patterns)
def get_top_ips(self, limit: int = 10) -> List[Tuple[str, int]]:
"""Get top N IP addresses by access count"""
return sorted(self.ip_counts.items(), key=lambda x: x[1], reverse=True)[:limit]
def get_top_paths(self, limit: int = 10) -> List[Tuple[str, int]]:
"""Get top N paths by access count"""
return sorted(self.path_counts.items(), key=lambda x: x[1], reverse=True)[:limit]
def get_top_user_agents(self, limit: int = 10) -> List[Tuple[str, int]]:
"""Get top N user agents by access count"""
return sorted(self.user_agent_counts.items(), key=lambda x: x[1], reverse=True)[:limit]
def get_suspicious_accesses(self, limit: int = 20) -> List[Dict]:
"""Get recent suspicious accesses"""
suspicious = [log for log in self.access_log if log.get('suspicious', False)]
return suspicious[-limit:]
def get_honeypot_triggered_ips(self) -> List[Tuple[str, List[str]]]:
"""Get IPs that accessed honeypot paths"""
return [(ip, paths) for ip, paths in self.honeypot_triggered.items()]
def get_stats(self) -> Dict:
"""Get statistics summary"""
suspicious_count = sum(1 for log in self.access_log if log.get('suspicious', False))
honeypot_count = sum(1 for log in self.access_log if log.get('honeypot_triggered', False))
return {
'total_accesses': len(self.access_log),
'unique_ips': len(self.ip_counts),
'unique_paths': len(self.path_counts),
'suspicious_accesses': suspicious_count,
'honeypot_triggered': honeypot_count,
'honeypot_ips': len(self.honeypot_triggered),
'top_ips': self.get_top_ips(10),
'top_paths': self.get_top_paths(10),
'top_user_agents': self.get_top_user_agents(10),
'recent_suspicious': self.get_suspicious_accesses(20),
'honeypot_triggered_ips': self.get_honeypot_triggered_ips()
}

123
src/wordlists.py Normal file
View File

@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""
Wordlists loader - reads all wordlists from wordlists.json
This allows easy customization without touching Python code.
"""
import json
import os
from pathlib import Path
class Wordlists:
"""Loads and provides access to wordlists from wordlists.json"""
def __init__(self):
self._data = self._load_config()
def _load_config(self):
"""Load wordlists from JSON file"""
config_path = Path(__file__).parent.parent / 'wordlists.json'
try:
with open(config_path, 'r') as f:
return json.load(f)
except FileNotFoundError:
print(f"⚠️ Warning: {config_path} not found, using default values")
return self._get_defaults()
except json.JSONDecodeError as e:
print(f"⚠️ Warning: Invalid JSON in {config_path}: {e}")
return self._get_defaults()
def _get_defaults(self):
"""Fallback default wordlists if JSON file is missing or invalid"""
return {
"usernames": {
"prefixes": ["admin", "user", "root"],
"suffixes": ["", "_prod", "_dev"]
},
"passwords": {
"prefixes": ["P@ssw0rd", "Admin"],
"simple": ["test", "demo", "password"]
},
"emails": {
"domains": ["example.com", "test.com"]
},
"api_keys": {
"prefixes": ["sk_live_", "api_", ""]
},
"databases": {
"names": ["production", "main_db"],
"hosts": ["localhost", "db.internal"]
},
"applications": {
"names": ["WebApp", "Dashboard"]
},
"users": {
"roles": ["Administrator", "User"]
}
}
@property
def username_prefixes(self):
return self._data.get("usernames", {}).get("prefixes", [])
@property
def username_suffixes(self):
return self._data.get("usernames", {}).get("suffixes", [])
@property
def password_prefixes(self):
return self._data.get("passwords", {}).get("prefixes", [])
@property
def simple_passwords(self):
return self._data.get("passwords", {}).get("simple", [])
@property
def email_domains(self):
return self._data.get("emails", {}).get("domains", [])
@property
def api_key_prefixes(self):
return self._data.get("api_keys", {}).get("prefixes", [])
@property
def database_names(self):
return self._data.get("databases", {}).get("names", [])
@property
def database_hosts(self):
return self._data.get("databases", {}).get("hosts", [])
@property
def application_names(self):
return self._data.get("applications", {}).get("names", [])
@property
def user_roles(self):
return self._data.get("users", {}).get("roles", [])
@property
def directory_files(self):
return self._data.get("directory_listing", {}).get("files", [])
@property
def directory_dirs(self):
return self._data.get("directory_listing", {}).get("directories", [])
@property
def error_codes(self):
return self._data.get("error_codes", [])
_wordlists_instance = None
def get_wordlists():
"""Get the singleton Wordlists instance"""
global _wordlists_instance
if _wordlists_instance is None:
_wordlists_instance = Wordlists()
return _wordlists_instance

197
wordlists.json Normal file
View File

@@ -0,0 +1,197 @@
{
"usernames": {
"prefixes": [
"admin",
"user",
"developer",
"root",
"system",
"db",
"api",
"service",
"deploy",
"test",
"prod",
"backup",
"monitor",
"jenkins",
"webapp"
],
"suffixes": [
"",
"_prod",
"_dev",
"_test",
"123",
"2024",
"_backup",
"_admin",
"01",
"02",
"_user",
"_service",
"_api"
]
},
"passwords": {
"prefixes": [
"P@ssw0rd",
"Passw0rd",
"Admin",
"Secret",
"Welcome",
"System",
"Database",
"Secure",
"Master",
"Root"
],
"simple": [
"test",
"demo",
"temp",
"change",
"password",
"admin",
"letmein",
"welcome",
"default",
"sample"
]
},
"emails": {
"domains": [
"example.com",
"company.com",
"localhost.com",
"test.com",
"domain.com",
"corporate.com",
"internal.net",
"enterprise.com",
"business.org"
]
},
"api_keys": {
"prefixes": [
"sk_live_",
"sk_test_",
"api_",
"key_",
"token_",
"access_",
"secret_",
"prod_",
""
]
},
"databases": {
"names": [
"production",
"prod_db",
"main_db",
"app_database",
"users_db",
"customer_data",
"analytics",
"staging_db",
"dev_database",
"wordpress",
"ecommerce",
"crm_db",
"inventory"
],
"hosts": [
"localhost",
"db.internal",
"mysql.local",
"postgres.internal",
"127.0.0.1",
"db-server-01",
"database.prod",
"sql.company.com"
]
},
"applications": {
"names": [
"WebApp",
"API Gateway",
"Dashboard",
"Admin Panel",
"CMS",
"Portal",
"Manager",
"Console",
"Control Panel",
"Backend"
]
},
"users": {
"roles": [
"Administrator",
"Developer",
"Manager",
"User",
"Guest",
"Moderator",
"Editor",
"Viewer",
"Analyst",
"Support"
]
},
"directory_listing": {
"files": [
"test.exe",
"backup.sql",
"database.sql",
"db_backup.sql",
"dump.sql",
"config.php",
"credentials.txt",
"passwords.txt",
"users.csv",
".env",
"id_rsa",
"id_rsa.pub",
"private_key.pem",
"api_keys.json",
"secrets.yaml",
"admin_notes.txt",
"settings.ini",
"database.yml",
"wp-config.php",
".htaccess",
"server.key",
"cert.pem",
"shadow.bak",
"passwd.old"
],
"directories": [
"uploads/",
"backups/",
"logs/",
"temp/",
"cache/",
"private/",
"config/",
"admin/",
"database/",
"backup/",
"old/",
"archive/",
".git/",
"keys/",
"credentials/"
]
},
"error_codes": [
400,
401,
403,
404,
500,
502,
503
]
}