diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..755671c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +*.so +*.egg +*.egg-info +dist +build +.git +.gitignore +README.md +*.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5d758cb --- /dev/null +++ b/.gitignore @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..adac20f --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md index a855c3d..498bc9e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,297 @@ -# Krawl -Krawl is a Web Honeypot & Deception server that aims to foul enumerations, web crawling, fuzzing and bruteforcing +

🕷️ Krawl

+ +

+ A modern, customizable zero-dependencies honeypot server designed to detect and track malicious activity through deceptive web pages, fake credentials, and canary tokens. +

+ +
+ + License + + + Release + +
+ +
+ + GitHub Container Registry + + + Kubernetes + + + Helm Chart + +
+ +
+ +

+ Overview • + Quick Start • + Configuration • + Dashboard • + Deception Techniques • + Contributing +

+ +![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://:/` + +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 + + +
+ +## 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. diff --git a/deployment.yaml b/deployment.yaml new file mode 100644 index 0000000..cb39eaf --- /dev/null +++ b/deployment.yaml @@ -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 diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..57c648d --- /dev/null +++ b/docker-compose.yaml @@ -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 diff --git a/helm/.helmignore b/helm/.helmignore new file mode 100644 index 0000000..6b8c0ab --- /dev/null +++ b/helm/.helmignore @@ -0,0 +1 @@ +.helmignore diff --git a/helm/Chart.yaml b/helm/Chart.yaml new file mode 100644 index 0000000..3fe5d8a --- /dev/null +++ b/helm/Chart.yaml @@ -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 diff --git a/helm/NOTES.txt b/helm/NOTES.txt new file mode 100644 index 0000000..6ba8fd9 --- /dev/null +++ b/helm/NOTES.txt @@ -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://:{{ .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://:{{ .Values.service.port }}/ + +{{- 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 }} \ No newline at end of file diff --git a/helm/templates/NOTES.txt b/helm/templates/NOTES.txt new file mode 100644 index 0000000..8ca65f6 --- /dev/null +++ b/helm/templates/NOTES.txt @@ -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://:{{ .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://:{{ .Values.service.port }}/ + +{{- 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 }} \ No newline at end of file diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl new file mode 100644 index 0000000..57f524a --- /dev/null +++ b/helm/templates/_helpers.tpl @@ -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 }} diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml new file mode 100644 index 0000000..f6fe92c --- /dev/null +++ b/helm/templates/configmap.yaml @@ -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 }} diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml new file mode 100644 index 0000000..bad4f0d --- /dev/null +++ b/helm/templates/deployment.yaml @@ -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 }} diff --git a/helm/templates/hpa.yaml b/helm/templates/hpa.yaml new file mode 100644 index 0000000..0f64b10 --- /dev/null +++ b/helm/templates/hpa.yaml @@ -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 }} diff --git a/helm/templates/ingress.yaml b/helm/templates/ingress.yaml new file mode 100644 index 0000000..ac72446 --- /dev/null +++ b/helm/templates/ingress.yaml @@ -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 }} diff --git a/helm/templates/network-policy.yaml b/helm/templates/network-policy.yaml new file mode 100644 index 0000000..085d847 --- /dev/null +++ b/helm/templates/network-policy.yaml @@ -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 }} diff --git a/helm/templates/secret.yaml b/helm/templates/secret.yaml new file mode 100644 index 0000000..798289c --- /dev/null +++ b/helm/templates/secret.yaml @@ -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 }} diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml new file mode 100644 index 0000000..340f905 --- /dev/null +++ b/helm/templates/service.yaml @@ -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 }} diff --git a/helm/templates/wordlists-configmap.yaml b/helm/templates/wordlists-configmap.yaml new file mode 100644 index 0000000..c37faca --- /dev/null +++ b/helm/templates/wordlists-configmap.yaml @@ -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 }} diff --git a/helm/values.yaml b/helm/values.yaml new file mode 100644 index 0000000..93d668b --- /dev/null +++ b/helm/values.yaml @@ -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 diff --git a/img/dashboard-1.png b/img/dashboard-1.png new file mode 100644 index 0000000..ad11dd8 Binary files /dev/null and b/img/dashboard-1.png differ diff --git a/img/dashboard-2.png b/img/dashboard-2.png new file mode 100644 index 0000000..65c0766 Binary files /dev/null and b/img/dashboard-2.png differ diff --git a/img/database.png b/img/database.png new file mode 100644 index 0000000..fea8b4f Binary files /dev/null and b/img/database.png differ diff --git a/img/deception-page.png b/img/deception-page.png new file mode 100644 index 0000000..1402143 Binary files /dev/null and b/img/deception-page.png differ diff --git a/manifests/configmap.yaml b/manifests/configmap.yaml new file mode 100644 index 0000000..42ba002 --- /dev/null +++ b/manifests/configmap.yaml @@ -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 \ No newline at end of file diff --git a/manifests/deployment.yaml b/manifests/deployment.yaml new file mode 100644 index 0000000..3fad020 --- /dev/null +++ b/manifests/deployment.yaml @@ -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 diff --git a/manifests/hpa.yaml b/manifests/hpa.yaml new file mode 100644 index 0000000..10bab0c --- /dev/null +++ b/manifests/hpa.yaml @@ -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 diff --git a/manifests/ingress.yaml b/manifests/ingress.yaml new file mode 100644 index 0000000..f5a6efc --- /dev/null +++ b/manifests/ingress.yaml @@ -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 diff --git a/manifests/kustomization.yaml b/manifests/kustomization.yaml new file mode 100644 index 0000000..8f41776 --- /dev/null +++ b/manifests/kustomization.yaml @@ -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 diff --git a/manifests/namespace.yaml b/manifests/namespace.yaml new file mode 100644 index 0000000..1cdb578 --- /dev/null +++ b/manifests/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: krawl-system diff --git a/manifests/network-policy.yaml b/manifests/network-policy.yaml new file mode 100644 index 0000000..e765b36 --- /dev/null +++ b/manifests/network-policy.yaml @@ -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 diff --git a/manifests/service.yaml b/manifests/service.yaml new file mode 100644 index 0000000..8db65b4 --- /dev/null +++ b/manifests/service.yaml @@ -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 diff --git a/manifests/wordlists-configmap.yaml b/manifests/wordlists-configmap.yaml new file mode 100644 index 0000000..4ff0b5d --- /dev/null +++ b/manifests/wordlists-configmap.yaml @@ -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 + ] + } diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..51391a9 --- /dev/null +++ b/src/config.py @@ -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)) + ) diff --git a/src/dashboard_template.py b/src/dashboard_template.py new file mode 100644 index 0000000..4bcde8b --- /dev/null +++ b/src/dashboard_template.py @@ -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'{i+1}{ip}{count}' + for i, (ip, count) in enumerate(stats['top_ips']) + ]) or 'No data' + + # Generate paths rows + top_paths_rows = '\n'.join([ + f'{i+1}{path}{count}' + for i, (path, count) in enumerate(stats['top_paths']) + ]) or 'No data' + + # Generate User-Agent rows + top_ua_rows = '\n'.join([ + f'{i+1}{ua[:80]}{count}' + for i, (ua, count) in enumerate(stats['top_user_agents']) + ]) or 'No data' + + # Generate suspicious accesses rows + suspicious_rows = '\n'.join([ + f'{log["ip"]}{log["path"]}{log["user_agent"][:60]}{log["timestamp"].split("T")[1][:8]}' + for log in stats['recent_suspicious'][-10:] + ]) or 'No suspicious activity detected' + + return f""" + + + + Krawl Dashboard + + + +
+

🕷️ Krawl Dashboard

+ +
+
+
{stats['total_accesses']}
+
Total Accesses
+
+
+
{stats['unique_ips']}
+
Unique IPs
+
+
+
{stats['unique_paths']}
+
Unique Paths
+
+
+
{stats['suspicious_accesses']}
+
Suspicious Accesses
+
+
+ +
+

⚠️ Recent Suspicious Activity

+ + + + + + + + + + + {suspicious_rows} + +
IP AddressPathUser-AgentTime
+
+ +
+

Top IP Addresses

+ + + + + + + + + + {top_ips_rows} + +
#IP AddressAccess Count
+
+ +
+

Top Paths

+ + + + + + + + + + {top_paths_rows} + +
#PathAccess Count
+
+ +
+

Top User-Agents

+ + + + + + + + + + {top_ua_rows} + +
#User-AgentCount
+
+
+ + +""" diff --git a/src/generators.py b/src/generators.py new file mode 100644 index 0000000..16c0c32 --- /dev/null +++ b/src/generators.py @@ -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) diff --git a/src/handler.py b/src/handler.py new file mode 100644 index 0000000..2768c6b --- /dev/null +++ b/src/handler.py @@ -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""" + + + + Krawl + + + +
+

Krawl me! 🕸

+
{Handler.counter}
+ + +
+ +""" + 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}") diff --git a/src/server.py b/src/server.py new file mode 100644 index 0000000..d10d33e --- /dev/null +++ b/src/server.py @@ -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 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() diff --git a/src/templates/dashboard_template.py b/src/templates/dashboard_template.py new file mode 100644 index 0000000..d4c6421 --- /dev/null +++ b/src/templates/dashboard_template.py @@ -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'{i+1}{ip}{count}' + for i, (ip, count) in enumerate(stats['top_ips']) + ]) or 'No data' + + # Generate paths rows + top_paths_rows = '\n'.join([ + f'{i+1}{path}{count}' + for i, (path, count) in enumerate(stats['top_paths']) + ]) or 'No data' + + # Generate User-Agent rows + top_ua_rows = '\n'.join([ + f'{i+1}{ua[:80]}{count}' + for i, (ua, count) in enumerate(stats['top_user_agents']) + ]) or 'No data' + + # Generate suspicious accesses rows + suspicious_rows = '\n'.join([ + f'{log["ip"]}{log["path"]}{log["user_agent"][:60]}{log["timestamp"].split("T")[1][:8]}' + for log in stats['recent_suspicious'][-10:] + ]) or 'No suspicious activity detected' + + # Generate honeypot triggered IPs rows + honeypot_rows = '\n'.join([ + f'{ip}{", ".join(paths)}{len(paths)}' + for ip, paths in stats.get('honeypot_triggered_ips', []) + ]) or 'No honeypot triggers yet' + + return f""" + + + + Krawl Dashboard + + + +
+

🕷️ Krawl Dashboard

+ +
+
+
{stats['total_accesses']}
+
Total Accesses
+
+
+
{stats['unique_ips']}
+
Unique IPs
+
+
+
{stats['unique_paths']}
+
Unique Paths
+
+
+
{stats['suspicious_accesses']}
+
Suspicious Accesses
+
+
+
{stats.get('honeypot_ips', 0)}
+
Honeypot Caught
+
+
+ +
+

🍯 Honeypot Triggers

+ + + + + + + + + + {honeypot_rows} + +
IP AddressAccessed PathsCount
+
+ +
+

⚠️ Recent Suspicious Activity

+ + + + + + + + + + + {suspicious_rows} + +
IP AddressPathUser-AgentTime
+
+ +
+

Top IP Addresses

+ + + + + + + + + + {top_ips_rows} + +
#IP AddressAccess Count
+
+ +
+

Top Paths

+ + + + + + + + + + {top_paths_rows} + +
#PathAccess Count
+
+ +
+

Top User-Agents

+ + + + + + + + + + {top_ua_rows} + +
#User-AgentCount
+
+
+ + +""" diff --git a/src/templates/html_templates.py b/src/templates/html_templates.py new file mode 100644 index 0000000..e17df75 --- /dev/null +++ b/src/templates/html_templates.py @@ -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 """ + + + + Admin Login + + + + + +""" + + +def login_error() -> str: + """Generate fake login error page""" + return """ + + + + Login Failed + + + + + +""" + + +def wordpress() -> str: + """Generate fake WordPress page""" + return """ + + + + + My Blog – Just another WordPress site + + + + + + + + +
+ + +
+
+
+

Hello world!

+ +
+
+

Welcome to WordPress. This is your first post. Edit or delete it, then start writing!

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

+
+
+ +
+
+

About This Site

+ +
+
+

This is a sample page. You can use it to write about your site, yourself, or anything else you'd like.

+
+
+
+ + +
+ + +""" + + +def phpmyadmin() -> str: + """Generate fake phpMyAdmin page""" + return """ + + + phpMyAdmin + + + +

phpMyAdmin

+ + +""" + + +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""" + +Index of {path} + + + +

Index of {path}

+ + + +""" + + for d in dirs: + html += f'\n' + + for f, size in files: + html += f'\n' + + html += '
NameLast ModifiedSize
Parent Directory--
{d}2024-12-01 10:30-
{f}2024-12-01 14:22{size}
' + return html diff --git a/src/tracker.py b/src/tracker.py new file mode 100644 index 0000000..8a73a4c --- /dev/null +++ b/src/tracker.py @@ -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() + } diff --git a/src/wordlists.py b/src/wordlists.py new file mode 100644 index 0000000..b0a9e1a --- /dev/null +++ b/src/wordlists.py @@ -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 + diff --git a/wordlists.json b/wordlists.json new file mode 100644 index 0000000..f1aae81 --- /dev/null +++ b/wordlists.json @@ -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 + ] +}