Merge pull request #64 from BlessedRebuS/dev

Krawl release v1.0.0
This commit is contained in:
Patrick Di Fazio
2026-01-29 14:45:04 +01:00
committed by GitHub
78 changed files with 10652 additions and 1698 deletions

87
.github/workflows/docker-build-push.yml vendored Normal file
View File

@@ -0,0 +1,87 @@
name: Build and Push Docker Image
on:
push:
branches:
- main
- beta
- dev
- github-actions-ci
paths:
- 'src/**'
- 'helm/Chart.yaml'
- 'config.yaml'
- 'Dockerfile'
- 'requirements.txt'
- 'entrypoint.sh'
- '.github/workflows/docker-build-push.yml'
tags:
- 'v*.*.*'
release:
types: [published]
workflow_dispatch:
env:
REGISTRY: ${{ vars.DOCKER_REGISTRY }}
IMAGE_NAME: ${{ vars.DOCKER_IMAGE_NAME }}
jobs:
build-and-push:
runs-on: self-hosted
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract appVersion from Chart.yaml and determine tags
id: tags
run: |
APP_VERSION=$(grep '^appVersion:' helm/Chart.yaml | awk '{print $2}' | tr -d '"' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -z "$APP_VERSION" ]; then
echo "Error: Could not extract appVersion from Chart.yaml"
exit 1
fi
if [[ "${{ github.ref_name }}" == "main" ]]; then
TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${APP_VERSION},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"
else
TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${APP_VERSION}-${{ github.ref_name }}"
fi
echo "tags=$TAGS" >> $GITHUB_OUTPUT
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.tags.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
- name: Image digest
run: |
echo "Image built and pushed with tags:"
echo "${{ steps.tags.outputs.tags }}"

76
.github/workflows/helm-package-push.yml vendored Normal file
View File

@@ -0,0 +1,76 @@
name: Package and Push Helm Chart
on:
push:
branches:
- main
- beta
- dev
- github-actions-ci
paths:
- 'helm/**'
- '.github/workflows/helm-package-push.yml'
tags:
- 'v*'
release:
types:
- published
- created
workflow_dispatch:
env:
REGISTRY: ${{ vars.DOCKER_REGISTRY }}
jobs:
package-and-push:
runs-on: self-hosted
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Helm
uses: azure/setup-helm@v4
with:
version: 'latest'
- name: Log in to Container Registry
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io --username ${{ github.actor }} --password-stdin
- name: Set Helm chart version and package
run: |
CHART_NAME=$(grep '^name:' ./helm/Chart.yaml | awk '{print $2}')
BASE_VERSION=$(grep '^version:' ./helm/Chart.yaml | awk '{print $2}')
if [[ "${{ github.ref_name }}" == "main" ]]; then
CHART_VERSION="${BASE_VERSION}"
else
CHART_VERSION="${BASE_VERSION}-${{ github.ref_name }}"
fi
# Update Chart.yaml temporarily with the versioned name
sed -i "s/^version:.*/version: ${CHART_VERSION}/" ./helm/Chart.yaml
# Package the helm chart
helm package ./helm
echo "CHART_NAME=${CHART_NAME}" >> $GITHUB_ENV
echo "CHART_VERSION=${CHART_VERSION}" >> $GITHUB_ENV
- name: Push Helm chart to registry
run: |
helm push ${{ env.CHART_NAME }}-${{ env.CHART_VERSION }}.tgz oci://${{ env.REGISTRY }}
- name: Chart pushed
run: |
CHART_VERSION=$(grep '^version:' ./helm/Chart.yaml | awk '{print $2}')
CHART_FILE=$(grep '^name:' ./helm/Chart.yaml | awk '{print $2}')
if [[ "${{ github.ref_name }}" == "main" ]]; then
echo "Chart pushed: ${CHART_FILE}:${CHART_VERSION}"
else
echo "Chart pushed: ${CHART_FILE}:${CHART_VERSION}-${{ github.ref_name }}"
fi

View File

@@ -0,0 +1,57 @@
name: Kubernetes Validation
on:
pull_request:
branches:
- main
- beta
- dev
paths:
- 'kubernetes/**'
- 'helm/**'
- '.github/workflows/kubernetes-validation.yml'
permissions:
contents: read
jobs:
validate-manifests:
name: Validate Kubernetes Manifests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate YAML syntax
run: |
for manifest in kubernetes/**/*.yaml; do
if [ -f "$manifest" ]; then
echo "Validating YAML syntax: $manifest"
python3 -c "import yaml, sys; yaml.safe_load(open('$manifest'))" || exit 1
fi
done
- name: Validate manifest structure
run: |
for manifest in kubernetes/**/*.yaml; do
if [ -f "$manifest" ]; then
echo "Checking $manifest"
if ! grep -q "kind:" "$manifest"; then
echo "Error: $manifest does not contain a Kubernetes kind"
exit 1
fi
fi
done
validate-helm:
name: Validate Helm Chart
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: azure/setup-helm@v4
- name: Helm lint
run: helm lint ./helm
- name: Helm template validation
run: helm template krawl ./helm > /tmp/helm-output.yaml

47
.github/workflows/pr-checks.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: PR Checks
on:
pull_request:
branches:
- main
- beta
- dev
permissions:
contents: read
pull-requests: read
jobs:
lint-and-test:
name: Lint & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install black flake8 pylint pytest
- name: Black format check
run: |
if ! black --check src/; then
echo "Run 'black src/' to format code"
black --diff src/
exit 1
fi
build-docker:
name: Build Docker
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t krawl:test .

59
.github/workflows/security-scan.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
name: Security Scan
on:
pull_request:
branches:
- main
- beta
- dev
permissions:
contents: read
jobs:
security-checks:
name: Security & Dependencies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install bandit safety
- name: Bandit security check
run: |
bandit -r src/ -f txt | tee bandit-report.txt
# Extract HIGH severity (not confidence) - look for the severity section
SEVERITY_SECTION=$(sed -n '/Total issues (by severity):/,/Total issues (by confidence):/p' bandit-report.txt)
HIGH_COUNT=$(echo "$SEVERITY_SECTION" | grep "High:" | grep -o "[0-9]*" | head -1)
if [ -z "$HIGH_COUNT" ]; then
HIGH_COUNT=0
fi
if [ "$HIGH_COUNT" -gt 0 ]; then
echo "Found $HIGH_COUNT HIGH severity security issues"
exit 1
fi
echo "✓ No HIGH severity security issues found"
- name: Safety check for dependencies
run: safety check --json || true
- name: Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'table'
severity: 'CRITICAL,HIGH'
exit-code: '1'

5
.gitignore vendored
View File

@@ -56,6 +56,7 @@ secrets/
.env
.env.local
.env.*.local
.envrc
# Logs
*.log
@@ -76,3 +77,7 @@ data/
# Personal canary tokens or sensitive configs
*canary*token*.yaml
personal-values.yaml
#exports dir (keeping .gitkeep so we have the dir)
/exports/*
/src/exports/*

View File

@@ -4,16 +4,26 @@ LABEL org.opencontainers.image.source=https://github.com/BlessedRebuS/Krawl
WORKDIR /app
# Install gosu for dropping privileges
RUN apt-get update && apt-get install -y --no-install-recommends gosu && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
COPY src/ /app/src/
COPY wordlists.json /app/
COPY entrypoint.sh /app/
COPY config.yaml /app/
RUN useradd -m -u 1000 krawl && \
chown -R krawl:krawl /app
USER krawl
mkdir -p /app/logs /app/data /app/exports && \
chown -R krawl:krawl /app && \
chmod +x /app/entrypoint.sh
EXPOSE 5000
ENV PYTHONUNBUFFERED=1
ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["python3", "src/server.py"]

300
README.md
View File

@@ -1,16 +1,16 @@
<h1 align="center">🕷️ Krawl</h1>
<h1 align="center">Krawl</h1>
<h3 align="center">
<a name="readme-top"></a>
<img
src="img/krawl-logo.jpg"
height="200"
src="img/krawl-svg.svg"
height="250"
>
</h3>
<div align="center">
<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.
A modern, customizable web honeypot server designed to detect and track malicious activity from attackers and web crawlers through deceptive web pages, fake credentials, and canary tokens.
</p>
<div align="center">
@@ -38,7 +38,7 @@
<p align="center">
<a href="#what-is-krawl">What is Krawl?</a> •
<a href="#-quick-start">Quick Start</a> •
<a href="#-installation">Installation</a> •
<a href="#honeypot-pages">Honeypot Pages</a> •
<a href="#dashboard">Dashboard</a> •
<a href="./ToDo.md">Todo</a> •
@@ -55,7 +55,7 @@ Tip: crawl the `robots.txt` paths for additional fun
## What is Krawl?
**Krawl** is a cloudnative deception server designed to detect, delay, and analyze malicious web crawlers and automated scanners.
**Krawl** is a cloudnative deception server designed to detect, delay, and analyze malicious attackers, web crawlers and automated scanners.
It creates realistic fake web applications filled with lowhanging fruit such as admin panels, configuration files, and exposed fake credentials to attract and identify suspicious activity.
@@ -68,156 +68,197 @@ It features:
- **Honeypot Paths**: Advertised in robots.txt to catch scanners
- **Fake Credentials**: Realistic-looking usernames, passwords, API keys
- **[Canary Token](#customizing-the-canary-token) Integration**: External alert triggering
- **Random server headers**: Confuse attacks based on server header and version
- **Real-time Dashboard**: Monitor suspicious activity
- **Customizable Wordlists**: Easy JSON-based configuration
- **Random Error Injection**: Mimic real server behavior
![asd](img/deception-page.png)
![dashboard](img/deception-page.png)
## 🚀 Quick Start
## Helm Chart
![geoip](img/geoip_dashboard.png)
Install with default values
## 🚀 Installation
```bash
helm install krawl oci://ghcr.io/blessedrebus/krawl-chart \
--namespace krawl-system \
--create-namespace
```
### Docker Run
Install with custom [canary token](#customizing-the-canary-token)
```bash
helm install krawl oci://ghcr.io/blessedrebus/krawl-chart \
--namespace krawl-system \
--create-namespace \
--set config.canaryTokenUrl="http://your-canary-token-url"
```
To access the deception server
```bash
kubectl get svc krawl -n krawl-system
```
Once the EXTERNAL-IP is assigned, access your deception server at:
```
http://<EXTERNAL-IP>:5000
```
## Kubernetes / Kustomize
Apply all manifests with
```bash
kubectl apply -f https://raw.githubusercontent.com/BlessedRebuS/Krawl/refs/heads/main/manifests/krawl-all-in-one-deploy.yaml
```
Retrieve dashboard path with
```bash
kubectl get secret krawl-server -n krawl-system -o jsonpath='{.data.dashboard-path}' | base64 -d
```
Or clone the repo and apply the `manifest` folder with
```bash
kubectl apply -k manifests
```
## Docker
Run Krawl as a docker container with
Run Krawl with the latest image:
```bash
docker run -d \
-p 5000:5000 \
-e CANARY_TOKEN_URL="http://your-canary-token-url" \
-e KRAWL_PORT=5000 \
-e KRAWL_DELAY=100 \
-e KRAWL_DASHBOARD_SECRET_PATH="/my-secret-dashboard" \
-e KRAWL_DATABASE_RETENTION_DAYS=30 \
--name krawl \
ghcr.io/blessedrebus/krawl:latest
```
## Docker Compose
Run Krawl with docker-compose in the project folder with
Access the server at `http://localhost:5000`
### Docker Compose
Create a `docker-compose.yaml` file:
```yaml
services:
krawl:
image: ghcr.io/blessedrebus/krawl:latest
container_name: krawl-server
ports:
- "5000:5000"
environment:
- CONFIG_LOCATION=config.yaml
- TZ="Europe/Rome"
volumes:
- ./config.yaml:/app/config.yaml:ro
- krawl-data:/app/data
restart: unless-stopped
volumes:
krawl-data:
```
Run with:
```bash
docker-compose up -d
```
Stop it with
Stop with:
```bash
docker-compose down
```
## Python 3.11+
### Kubernetes
**Krawl is also available natively on Kubernetes**. Installation can be done either [via manifest](kubernetes/README.md) or [using the helm chart](helm/README.md).
Clone the repository
## Use Krawl to Ban Malicious IPs
Krawl uses a reputation-based system to classify attacker IP addresses. Every five minutes, Krawl exports the identified malicious IPs to a `malicious_ips.txt` file.
This file can either be mounted from the Docker container into another system or downloaded directly via `curl`:
```bash
git clone https://github.com/blessedrebus/krawl.git
cd krawl/src
curl https://your-krawl-instance/<DASHBOARD-PATH>/api/download/malicious_ips.txt
```
Run the server
This file can be used to [update a set of firewall rules](https://www.allthingstech.ch/using-opnsense-and-ip-blocklists-to-block-malicious-traffic), for example on OPNsense and pfSense, enabling automatic blocking of malicious IPs or using IPtables
## IP Reputation
Krawl [uses tasks that analyze recent traffic to build and continuously update an IP reputation](src/tasks/analyze_ips.py) score. It runs periodically and evaluates each active IP address based on multiple behavioral indicators to classify it as an attacker, crawler, or regular user. Thresholds are fully customizable.
![ip reputation](img/ip-reputation.png)
The analysis includes:
- **Risky HTTP methods usage** (e.g. POST, PUT, DELETE ratios)
- **Robots.txt violations**
- **Request timing anomalies** (bursty or irregular patterns)
- **User-Agent consistency**
- **Attack URL detection** (e.g. SQL injection, XSS patterns)
Each signal contributes to a weighted scoring model that assigns a reputation category:
- `attacker`
- `bad_crawler`
- `good_crawler`
- `regular_user`
- `unknown` (for insufficient data)
The resulting scores and metrics are stored in the database and used by Krawl to drive dashboards, reputation tracking, and automated mitigation actions such as IP banning or firewall integration.
## Forward server header
If Krawl is deployed behind a proxy such as NGINX the **server header** should be forwarded using the following configuration in your proxy:
```bash
python3 server.py
location / {
proxy_pass https://your-krawl-instance;
proxy_pass_header Server;
}
```
Visit
## API
Krawl uses the following APIs
- https://iprep.lcrawl.com (IP Reputation)
- https://nominatim.openstreetmap.org/reverse (Reverse IP Lookup)
- https://api.ipify.org (Public IP discovery)
- http://ident.me (Public IP discovery)
- https://ifconfig.me (Public IP discovery)
`http://localhost:5000`
## Configuration
Krawl uses a **configuration hierarchy** in which **environment variables take precedence over the configuration file**. This approach is recommended for Docker deployments and quick out-of-the-box customization.
To access the dashboard
### Configuration via Enviromental Variables
`http://localhost:5000/<dashboard-secret-path>`
| Environment Variable | Description | Default |
|----------------------|-------------|---------|
| `CONFIG_LOCATION` | Path to yaml config file | `config.yaml` |
| `KRAWL_PORT` | Server listening port | `5000` |
| `KRAWL_DELAY` | Response delay in milliseconds | `100` |
| `KRAWL_SERVER_HEADER` | HTTP Server header for deception | `""` |
| `KRAWL_LINKS_LENGTH_RANGE` | Link length range as `min,max` | `5,15` |
| `KRAWL_LINKS_PER_PAGE_RANGE` | Links per page as `min,max` | `10,15` |
| `KRAWL_CHAR_SPACE` | Characters used for link generation | `abcdefgh...` |
| `KRAWL_MAX_COUNTER` | Initial counter value | `10` |
| `KRAWL_CANARY_TOKEN_URL` | External canary token URL | None |
| `KRAWL_CANARY_TOKEN_TRIES` | Requests before showing canary token | `10` |
| `KRAWL_DASHBOARD_SECRET_PATH` | Custom dashboard path | Auto-generated |
| `KRAWL_PROBABILITY_ERROR_CODES` | Error response probability (0-100%) | `0` |
| `KRAWL_DATABASE_PATH` | Database file location | `data/krawl.db` |
| `KRAWL_DATABASE_RETENTION_DAYS` | Days to retain data in database | `30` |
| `KRAWL_HTTP_RISKY_METHODS_THRESHOLD` | Threshold for risky HTTP methods detection | `0.1` |
| `KRAWL_VIOLATED_ROBOTS_THRESHOLD` | Threshold for robots.txt violations | `0.1` |
| `KRAWL_UNEVEN_REQUEST_TIMING_THRESHOLD` | Coefficient of variation threshold for timing | `0.5` |
| `KRAWL_UNEVEN_REQUEST_TIMING_TIME_WINDOW_SECONDS` | Time window for request timing analysis in seconds | `300` |
| `KRAWL_USER_AGENTS_USED_THRESHOLD` | Threshold for detecting multiple user agents | `2` |
| `KRAWL_ATTACK_URLS_THRESHOLD` | Threshold for attack URL detection | `1` |
| `KRAWL_INFINITE_PAGES_FOR_MALICIOUS` | Serve infinite pages to malicious IPs | `true` |
| `KRAWL_MAX_PAGES_LIMIT` | Maximum page limit for crawlers | `250` |
| `KRAWL_BAN_DURATION_SECONDS` | Ban duration in seconds for rate-limited IPs | `600` |
## Configuration via Environment Variables
For example
To customize the deception server installation several **environment variables** can be specified.
```bash
# Set canary token
export CONFIG_LOCATION="config.yaml"
export KRAWL_CANARY_TOKEN_URL="http://your-canary-token-url"
| 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` |
| `SERVER_HEADER` | HTTP Server header for deception | `Apache/2.2.22 (Ubuntu)` |
# Set number of pages range (min,max format)
export KRAWL_LINKS_PER_PAGE_RANGE="5,25"
# Set analyzer thresholds
export KRAWL_HTTP_RISKY_METHODS_THRESHOLD="0.2"
export KRAWL_VIOLATED_ROBOTS_THRESHOLD="0.15"
# Set custom dashboard path
export KRAWL_DASHBOARD_SECRET_PATH="/my-secret-dashboard"
```
Example of a Docker run with env variables:
```bash
docker run -d \
-p 5000:5000 \
-e KRAWL_PORT=5000 \
-e KRAWL_DELAY=100 \
-e KRAWL_CANARY_TOKEN_URL="http://your-canary-token-url" \
--name krawl \
ghcr.io/blessedrebus/krawl:latest
```
### Configuration via config.yaml
You can use the [config.yaml](config.yaml) file for more advanced configurations, such as Docker Compose or Helm chart deployments.
# Honeypot
Below is a complete overview of the Krawl honeypots capabilities
## 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
```
The actual (juicy) robots.txt configuration [is the following](src/templates/html/robots.txt).
## Honeypot pages
Requests to common admin endpoints (`/admin/`, `/wp-admin/`, `/phpMyAdmin/`) return a fake login page. Any login attempt triggers a 1-second delay to simulate real processing and is fully logged in the dashboard (credentials, IP, headers, timing).
<div align="center">
<img src="img/admin-page.png" width="60%" />
</div>
![admin page](img/admin-page.png)
Requests to paths like `/backup/`, `/config/`, `/database/`, `/private/`, or `/uploads/` return a fake directory listing populated with “interesting” files, each assigned a random file size to look realistic.
@@ -225,21 +266,23 @@ Requests to paths like `/backup/`, `/config/`, `/database/`, `/private/`, or `/u
The `.env` endpoint exposes fake database connection strings, **AWS API keys**, and **Stripe secrets**. It intentionally returns an error due to the `Content-Type` being `application/json` instead of plain text, mimicking a “juicy” misconfiguration that crawlers and scanners often flag as information leakage.
![env-page](img/env-page.png)
The `/server` page displays randomly generated fake error information for each known server.
![server and env page](img/server-and-env-page.png)
The pages `/api/v1/users` and `/api/v2/secrets` show fake users and random secrets in JSON format
<div align="center">
<img src="img/api-users-page.png" width="45%" style="vertical-align: middle; margin: 0 10px;" />
<img src="img/api-secrets-page.png" width="45%" style="vertical-align: middle; margin: 0 10px;" />
</div>
![users and secrets](img/users-and-secrets.png)
The pages `/credentials.txt` and `/passwords.txt` show fake users and random secrets
<div align="center">
<img src="img/credentials-page.png" width="35%" style="vertical-align: middle; margin: 0 10px;" />
<img src="img/passwords-page.png" width="45%" style="vertical-align: middle; margin: 0 10px;" />
</div>
![credentials and passwords](img/credentials-and-passwords.png)
Pages such as `/users`, `/search`, `/contact`, `/info`, `/input`, and `/feedback`, along with APIs like `/api/sql` and `/api/database`, are designed to lure attackers into performing attacks such as **SQL injection** or **XSS**.
![sql injection](img/sql_injection.png)
Automated tools like **SQLMap** will receive a different randomized database error on each request, increasing scan noise and confusing the attacker. All detected attacks are logged and displayed in the dashboard.
## Customizing the Canary Token
To create a custom canary token, visit https://canarytokens.org
@@ -280,11 +323,13 @@ Access the dashboard at `http://<server-ip>:<port>/<dashboard-path>`
The dashboard shows:
- Total and unique accesses
- Suspicious activity detection
- Top IPs, paths, and user-agents
- Suspicious activity and attack detection
- Top IPs, paths, user-agents and GeoIP localization
- Real-time monitoring
The attackers' triggered honeypot path and the suspicious activity (such as failed login attempts) are logged
The attackers access to the honeypot endpoint and related suspicious activities (such as failed login attempts) are logged.
Krawl also implements a scoring system designed to distinguish between malicious and legitimate behavior on the website.
![dashboard-1](img/dashboard-1.png)
@@ -292,14 +337,7 @@ The top IP Addresses is shown along with top paths and User Agents
![dashboard-2](img/dashboard-2.png)
### Retrieving Dashboard Path
Check server startup logs or get the secret with
```bash
kubectl get secret krawl-server -n krawl-system \
-o jsonpath='{.data.dashboard-path}' | base64 -d && echo
```
![dashboard-3](img/dashboard-3.png)
## 🤝 Contributing

46
config.yaml Normal file
View File

@@ -0,0 +1,46 @@
# Krawl Honeypot Configuration
server:
port: 5000
delay: 100 # Response delay in milliseconds
# manually set the server header, if null a random one will be used.
server_header: null
links:
min_length: 5
max_length: 15
min_per_page: 5
max_per_page: 10
char_space: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
max_counter: 10
canary:
token_url: null # Optional canary token URL
token_tries: 10
dashboard:
# if set to "null" this will Auto-generates random path if not set
# can be set to "/dashboard" or similar <-- note this MUST include a forward slash
# secret_path: super-secret-dashboard-path
secret_path: test
database:
path: "data/krawl.db"
retention_days: 30
behavior:
probability_error_codes: 0 # 0-100 percentage
analyzer:
http_risky_methods_threshold: 0.1
violated_robots_threshold: 0.1
uneven_request_timing_threshold: 0.5
uneven_request_timing_time_window_seconds: 300
user_agents_used_threshold: 2
attack_urls_threshold: 1
crawl:
infinite_pages_for_malicious: true
max_pages_limit: 250
ban_duration_seconds: 600

View File

@@ -1,5 +1,4 @@
version: '3.8'
---
services:
krawl:
build:
@@ -8,27 +7,26 @@ services:
container_name: krawl-server
ports:
- "5000:5000"
environment:
- CONFIG_LOCATION=config.yaml
# set this to change timezone, alternatively mount /etc/timezone or /etc/localtime based on the time system management of the host environment
# - TZ=${TZ}
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
- SERVER_HEADER=Apache/2.2.22 (Ubuntu)
# 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
- ./config.yaml:/app/config.yaml:ro
- ./logs:/app/logs
- ./exports:/app/exports
- data:/app/data
restart: unless-stopped
healthcheck:
test: ["CMD", "python3", "-c", "import requests; requests.get('http://localhost:5000')"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
develop:
watch:
- path: ./Dockerfile
action: rebuild
- path: ./src/
action: sync+restart
target: /app/src
- path: ./docker-compose.yaml
action: rebuild
volumes:
data:

90
docs/coding-guidelines.md Normal file
View File

@@ -0,0 +1,90 @@
### Coding Standards
**Style & Structure**
- Prefer longer, explicit code over compact one-liners
- Always include docstrings for functions/classes + inline comments
- Strongly prefer OOP-style code (classes over functional/nested functions)
- Strong typing throughout (dataclasses, TypedDict, Enums, type hints)
- Value future-proofing and expanded usage insights
**Data Design**
- Use dataclasses for internal data modeling
- Typed JSON structures
- Functions return fully typed objects (no loose dicts)
- Snapshot files in JSON or YAML
- Human-readable fields (e.g., `sql_injection`, `xss_attempt`)
**Templates & UI**
- Don't mix large HTML/CSS blocks in Python code
- Prefer Jinja templates for HTML rendering
- Clean CSS, minimal inline clutter, readable template logic
**Writing & Documentation**
- Markdown documentation
- Clear section headers
- Roadmap/Phase/Feature-Session style documents
**Logging**
- Use singleton for logging found in `src\logger.py`
- Setup logging at app start:
```
initialize_logging()
app_logger = get_app_logger()
access_logger = get_access_logger()
credential_logger = get_credential_logger()
```
**Preferred Pip Packages**
- API/Web Server: Simple Python
- HTTP: Requests
- SQLite: Sqlalchemy
- Database Migrations: Alembic
### Error Handling
- Custom exception classes for domain-specific errors
- Consistent error response formats (JSON structure)
- Logging severity levels (ERROR vs WARNING)
### Configuration
- `.env` for secrets (never committed)
- Maintain `.env.example` in each component for documentation
- Typed config loaders using dataclasses
- Validation on startup
### Containerization & Deployment
- Explicit Dockerfiles
- Production-friendly hardening (distroless/slim when meaningful)
- Use git branch as tag
### Dependency Management
- Use `requirements.txt` and virtual environments (`python3 -m venv venv`)
- Use path `venv` for all virtual environments
- Pin versions to version ranges (or exact versions if pinning a particular version)
- Activate venv before running code (unless in Docker)
### Testing Standards
- Manual testing preferred for applications
- **tests:** Use shell scripts with curl/httpie for simulation and attack scripts.
- tests should be located in `tests` directory
### Git Standards
**Branch Strategy:**
- `master` - Production-ready code only
- `beta` - Public pre-release testing
- `dev` - Main development branch, integration point
**Workflow:**
- Feature work branches off `dev` (e.g., `feature/add-scheduler`)
- Merge features back to `dev` for testing
- Promote `dev` → `beta` for public testing (when applicable)
- Promote `beta` (or `dev`) → `master` for production
**Commit Messages:**
- Use conventional commit format: `feat:`, `fix:`, `docs:`, `refactor:`, etc.
- Keep commits atomic and focused
- Write clear, descriptive messages
**Tagging:**
- Tag releases on `master` with semantic versioning (e.g., `v1.2.3`)
- Optionally tag beta releases (e.g., `v1.2.3-beta.1`)

8
entrypoint.sh Normal file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
set -e
# Fix ownership of mounted directories
chown -R krawl:krawl /app/logs /app/data /app/exports 2>/dev/null || true
# Drop to krawl user and run the application
exec gosu krawl "$@"

View File

@@ -2,8 +2,8 @@ apiVersion: v2
name: krawl-chart
description: A Helm chart for Krawl honeypot server
type: application
version: 0.1.2
appVersion: "1.0.0"
version: 1.0.0
appVersion: 1.0.0
keywords:
- honeypot
- security
@@ -13,3 +13,4 @@ maintainers:
home: https://github.com/blessedrebus/krawl
sources:
- https://github.com/blessedrebus/krawl
icon: https://raw.githubusercontent.com/blessedrebus/krawl/main/img/krawl-svg.svg

356
helm/README.md Normal file
View File

@@ -0,0 +1,356 @@
# Krawl Helm Chart
A Helm chart for deploying the Krawl honeypot application on Kubernetes.
## Prerequisites
- Kubernetes 1.19+
- Helm 3.0+
- Persistent Volume provisioner (optional, for database persistence)
## Installation
### Helm Chart
Install with default values:
```bash
helm install krawl oci://ghcr.io/blessedrebus/krawl-chart \
--version 1.0.0 \
--namespace krawl-system \
--create-namespace
```
Or create a minimal `values.yaml` file:
```yaml
service:
type: LoadBalancer
port: 5000
timezone: "Europe/Rome"
ingress:
enabled: true
className: "traefik"
hosts:
- host: krawl.example.com
paths:
- path: /
pathType: Prefix
config:
server:
port: 5000
delay: 100
dashboard:
secret_path: null # Auto-generated if not set
database:
persistence:
enabled: true
size: 1Gi
```
Install with custom values:
```bash
helm install krawl oci://ghcr.io/blessedrebus/krawl-chart \
--version 0.2.2 \
--namespace krawl-system \
--create-namespace \
-f values.yaml
```
To access the deception server:
```bash
kubectl get svc krawl -n krawl-system
```
Once the EXTERNAL-IP is assigned, access your deception server at `http://<EXTERNAL-IP>:5000`
### Add the repository (if applicable)
```bash
helm repo add krawl https://github.com/BlessedRebuS/Krawl
helm repo update
```
### Install from OCI Registry
```bash
helm install krawl oci://ghcr.io/blessedrebus/krawl-chart --version 0.2.1
```
Or with a specific namespace:
```bash
helm install krawl oci://ghcr.io/blessedrebus/krawl-chart --version 0.2.1 -n krawl --create-namespace
```
### Install the chart locally
```bash
helm install krawl ./helm
```
### Install with custom values
```bash
helm install krawl ./helm -f values.yaml
```
### Install in a specific namespace
```bash
helm install krawl ./helm -n krawl --create-namespace
```
## Configuration
The following table lists the main configuration parameters of the Krawl chart and their default values.
### Global Settings
| Parameter | Description | Default |
|-----------|-------------|---------|
| `replicaCount` | Number of pod replicas | `1` |
| `image.repository` | Image repository | `ghcr.io/blessedrebus/krawl` |
| `image.tag` | Image tag | `latest` |
| `image.pullPolicy` | Image pull policy | `Always` |
### Service Configuration
| Parameter | Description | Default |
|-----------|-------------|---------|
| `service.type` | Service type | `LoadBalancer` |
| `service.port` | Service port | `5000` |
| `service.externalTrafficPolicy` | External traffic policy | `Local` |
### Ingress Configuration
| Parameter | Description | Default |
|-----------|-------------|---------|
| `ingress.enabled` | Enable ingress | `true` |
| `ingress.className` | Ingress class name | `traefik` |
| `ingress.hosts[0].host` | Ingress hostname | `krawl.example.com` |
### Server Configuration
| Parameter | Description | Default |
|-----------|-------------|---------|
| `config.server.port` | Server port | `5000` |
| `config.server.delay` | Response delay in milliseconds | `100` |
| `config.server.timezone` | IANA timezone (e.g., "America/New_York") | `null` |
### Links Configuration
| Parameter | Description | Default |
|-----------|-------------|---------|
| `config.links.min_length` | Minimum link length | `5` |
| `config.links.max_length` | Maximum link length | `15` |
| `config.links.min_per_page` | Minimum links per page | `10` |
| `config.links.max_per_page` | Maximum links per page | `15` |
| `config.links.char_space` | Character space for link generation | `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789` |
| `config.links.max_counter` | Maximum counter value | `10` |
### Canary Configuration
| Parameter | Description | Default |
|-----------|-------------|---------|
| `config.canary.token_url` | Canary token URL | `null` |
| `config.canary.token_tries` | Number of canary token tries | `10` |
### Dashboard Configuration
| Parameter | Description | Default |
|-----------|-------------|---------|
| `config.dashboard.secret_path` | Secret dashboard path (auto-generated if null) | `null` |
### API Configuration
| Parameter | Description | Default |
|-----------|-------------|---------|
| `config.api.server_url` | API server URL | `null` |
| `config.api.server_port` | API server port | `8080` |
| `config.api.server_path` | API server path | `/api/v2/users` |
### Database Configuration
| Parameter | Description | Default |
|-----------|-------------|---------|
| `config.database.path` | Database file path | `data/krawl.db` |
| `config.database.retention_days` | Data retention in days | `30` |
| `database.persistence.enabled` | Enable persistent volume | `true` |
| `database.persistence.size` | Persistent volume size | `1Gi` |
| `database.persistence.accessMode` | Access mode | `ReadWriteOnce` |
### Behavior Configuration
| Parameter | Description | Default |
|-----------|-------------|---------|
| `config.behavior.probability_error_codes` | Error code probability (0-100) | `0` |
### Analyzer Configuration
| Parameter | Description | Default |
|-----------|-------------|---------|
| `config.analyzer.http_risky_methods_threshold` | HTTP risky methods threshold | `0.1` |
| `config.analyzer.violated_robots_threshold` | Violated robots.txt threshold | `0.1` |
| `config.analyzer.uneven_request_timing_threshold` | Uneven request timing threshold | `0.5` |
| `config.analyzer.uneven_request_timing_time_window_seconds` | Time window for request timing analysis | `300` |
| `config.analyzer.user_agents_used_threshold` | User agents threshold | `2` |
| `config.analyzer.attack_urls_threshold` | Attack URLs threshold | `1` |
### Crawl Configuration
| Parameter | Description | Default |
|-----------|-------------|---------|
| `config.crawl.infinite_pages_for_malicious` | Infinite pages for malicious crawlers | `true` |
| `config.crawl.max_pages_limit` | Maximum pages limit for legitimate crawlers | `250` |
| `config.crawl.ban_duration_seconds` | IP ban duration in seconds | `600` |
### Resource Limits
| Parameter | Description | Default |
|-----------|-------------|---------|
| `resources.limits.cpu` | CPU limit | `500m` |
| `resources.limits.memory` | Memory limit | `256Mi` |
| `resources.requests.cpu` | CPU request | `100m` |
| `resources.requests.memory` | Memory request | `64Mi` |
### Autoscaling
| Parameter | Description | Default |
|-----------|-------------|---------|
| `autoscaling.enabled` | Enable horizontal pod autoscaling | `false` |
| `autoscaling.minReplicas` | Minimum replicas | `1` |
| `autoscaling.maxReplicas` | Maximum replicas | `1` |
| `autoscaling.targetCPUUtilizationPercentage` | Target CPU utilization | `70` |
| `autoscaling.targetMemoryUtilizationPercentage` | Target memory utilization | `80` |
### Network Policy
| Parameter | Description | Default |
|-----------|-------------|---------|
| `networkPolicy.enabled` | Enable network policy | `true` |
### Retrieving Dashboard Path
Check server startup logs or get the secret with
```bash
kubectl get secret krawl-server -n krawl-system \
-o jsonpath='{.data.dashboard-path}' | base64 -d && echo
```
## Usage Examples
### Basic Installation
```bash
helm install krawl ./helm
```
### Installation with Custom Domain
```bash
helm install krawl ./helm \
--set ingress.hosts[0].host=honeypot.example.com
```
### Enable Canary Tokens
```bash
helm install krawl ./helm \
--set config.canary.token_url=https://canarytokens.com/your-token
```
### Configure Custom API Endpoint
```bash
helm install krawl ./helm \
--set config.api.server_url=https://api.example.com \
--set config.api.server_port=443
```
### Create Values Override File
Create `custom-values.yaml`:
```yaml
config:
server:
port: 8080
delay: 500
canary:
token_url: https://your-canary-token-url
dashboard:
secret_path: /super-secret-path
crawl:
max_pages_limit: 500
ban_duration_seconds: 3600
```
Then install:
```bash
helm install krawl ./helm -f custom-values.yaml
```
## Upgrading
```bash
helm upgrade krawl ./helm
```
## Uninstalling
```bash
helm uninstall krawl
```
## Troubleshooting
### Check chart syntax
```bash
helm lint ./helm
```
### Dry run to verify values
```bash
helm install krawl ./helm --dry-run --debug
```
### Check deployed configuration
```bash
kubectl get configmap krawl-config -o yaml
```
### View pod logs
```bash
kubectl logs -l app.kubernetes.io/name=krawl
```
## Chart Files
- `Chart.yaml` - Chart metadata
- `values.yaml` - Default configuration values
- `templates/` - Kubernetes resource templates
- `deployment.yaml` - Krawl deployment
- `service.yaml` - Service configuration
- `configmap.yaml` - Application configuration
- `pvc.yaml` - Persistent volume claim
- `ingress.yaml` - Ingress configuration
- `hpa.yaml` - Horizontal pod autoscaler
- `network-policy.yaml` - Network policies
## Support
For issues and questions, please visit the [Krawl GitHub repository](https://github.com/BlessedRebuS/Krawl).

View File

@@ -5,14 +5,36 @@ metadata:
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 }}
SERVER_HEADER: {{ .Values.config.serverHeader | quote }}
CANARY_TOKEN_URL: {{ .Values.config.canaryTokenUrl | quote }}
config.yaml: |
# Krawl Honeypot Configuration
server:
port: {{ .Values.config.server.port }}
delay: {{ .Values.config.server.delay }}
links:
min_length: {{ .Values.config.links.min_length }}
max_length: {{ .Values.config.links.max_length }}
min_per_page: {{ .Values.config.links.min_per_page }}
max_per_page: {{ .Values.config.links.max_per_page }}
char_space: {{ .Values.config.links.char_space | quote }}
max_counter: {{ .Values.config.links.max_counter }}
canary:
token_url: {{ .Values.config.canary.token_url | toYaml }}
token_tries: {{ .Values.config.canary.token_tries }}
dashboard:
secret_path: {{ .Values.config.dashboard.secret_path | toYaml }}
database:
path: {{ .Values.config.database.path | quote }}
retention_days: {{ .Values.config.database.retention_days }}
behavior:
probability_error_codes: {{ .Values.config.behavior.probability_error_codes }}
analyzer:
http_risky_methods_threshold: {{ .Values.config.analyzer.http_risky_methods_threshold }}
violated_robots_threshold: {{ .Values.config.analyzer.violated_robots_threshold }}
uneven_request_timing_threshold: {{ .Values.config.analyzer.uneven_request_timing_threshold }}
uneven_request_timing_time_window_seconds: {{ .Values.config.analyzer.uneven_request_timing_time_window_seconds }}
user_agents_used_threshold: {{ .Values.config.analyzer.user_agents_used_threshold }}
attack_urls_threshold: {{ .Values.config.analyzer.attack_urls_threshold }}
crawl:
infinite_pages_for_malicious: {{ .Values.config.crawl.infinite_pages_for_malicious }}
max_pages_limit: {{ .Values.config.crawl.max_pages_limit }}
ban_duration_seconds: {{ .Values.config.crawl.ban_duration_seconds }}

View File

@@ -38,30 +38,49 @@ spec:
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.config.port }}
containerPort: {{ .Values.config.server.port }}
protocol: TCP
envFrom:
- configMapRef:
name: {{ include "krawl.fullname" . }}-config
env:
- name: DASHBOARD_SECRET_PATH
valueFrom:
secretKeyRef:
name: {{ include "krawl.fullname" . }}
key: dashboard-path
- name: CONFIG_LOCATION
value: "config.yaml"
{{- if .Values.timezone }}
- name: TZ
value: {{ .Values.timezone | quote }}
{{- end }}
volumeMounts:
- name: config
mountPath: /app/config.yaml
subPath: config.yaml
readOnly: true
- name: wordlists
mountPath: /app/wordlists.json
subPath: wordlists.json
readOnly: true
{{- if .Values.database.persistence.enabled }}
- name: database
mountPath: /app/data
{{- end }}
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
volumes:
- name: config
configMap:
name: {{ include "krawl.fullname" . }}-config
- name: wordlists
configMap:
name: {{ include "krawl.fullname" . }}-wordlists
{{- if .Values.database.persistence.enabled }}
- name: database
{{- if .Values.database.persistence.existingClaim }}
persistentVolumeClaim:
claimName: {{ .Values.database.persistence.existingClaim }}
{{- else }}
persistentVolumeClaim:
claimName: {{ include "krawl.fullname" . }}-db
{{- end }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}

17
helm/templates/pvc.yaml Normal file
View File

@@ -0,0 +1,17 @@
{{- if and .Values.database.persistence.enabled (not .Values.database.persistence.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "krawl.fullname" . }}-db
labels:
{{- include "krawl.labels" . | nindent 4 }}
spec:
accessModes:
- {{ .Values.database.persistence.accessMode }}
{{- if .Values.database.persistence.storageClassName }}
storageClassName: {{ .Values.database.persistence.storageClassName }}
{{- end }}
resources:
requests:
storage: {{ .Values.database.persistence.size }}
{{- end }}

View File

@@ -1,16 +0,0 @@
{{- $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

@@ -3,7 +3,7 @@ replicaCount: 1
image:
repository: ghcr.io/blessedrebus/krawl
pullPolicy: Always
tag: "latest"
tag: "1.0.0"
imagePullSecrets: []
nameOverride: "krawl"
@@ -49,6 +49,11 @@ resources:
cpu: 100m
memory: 64Mi
# Container timezone configuration
# Set this to change timezone (e.g., "America/New_York", "Europe/Rome")
# If not set, container will use its default timezone
timezone: ""
autoscaling:
enabled: false
minReplicas: 1
@@ -62,19 +67,53 @@ tolerations: []
affinity: {}
# Application configuration
# Application configuration (config.yaml structure)
config:
server:
port: 5000
delay: 100
linksMinLength: 5
linksMaxLength: 15
linksMinPerPage: 10
linksMaxPerPage: 15
maxCounter: 10
canaryTokenTries: 10
probabilityErrorCodes: 0
serverHeader: "Apache/2.2.22 (Ubuntu)"
# canaryTokenUrl: set-your-canary-token-url-here
links:
min_length: 5
max_length: 15
min_per_page: 10
max_per_page: 15
char_space: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
max_counter: 10
canary:
token_url: null # Set your canary token URL here
token_tries: 10
dashboard:
secret_path: null # Auto-generated if not set, or set to "/my-secret-dashboard"
database:
path: "data/krawl.db"
retention_days: 30
behavior:
probability_error_codes: 0
analyzer:
http_risky_methods_threshold: 0.1
violated_robots_threshold: 0.1
uneven_request_timing_threshold: 0.5
uneven_request_timing_time_window_seconds: 300
user_agents_used_threshold: 2
attack_urls_threshold: 1
crawl:
infinite_pages_for_malicious: true
max_pages_limit: 250
ban_duration_seconds: 600
# Database persistence configuration
database:
# Persistence configuration
persistence:
enabled: true
# Storage class name (use default if not specified)
# storageClassName: ""
# Access mode for the persistent volume
accessMode: ReadWriteOnce
# Size of the persistent volume
size: 1Gi
# Optional: Use existing PVC
# existingClaim: ""
networkPolicy:
enabled: true
@@ -268,6 +307,17 @@ wordlists:
- .git/
- keys/
- credentials/
server_headers:
- Apache/2.2.22 (Ubuntu)
- nginx/1.18.0
- Microsoft-IIS/10.0
- LiteSpeed
- Caddy
- Gunicorn/20.0.4
- uvicorn/0.13.4
- Express
- Flask/1.1.2
- Django/3.1
error_codes:
- 400
- 401

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 76 KiB

BIN
img/dashboard-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

BIN
img/geoip_dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

BIN
img/ip-reputation.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

95
img/krawl-svg.svg Normal file
View File

@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer1"><g
id="g21250"
transform="matrix(0.9765625,0,0,0.9765625,1536.0434,1186.1434)"
style="display:inline"><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1241.1385,-1007.2559 c -0.6853,-0.9666 -1.7404,-3.1071 -1.7404,-3.5311 0,-0.2316 -0.3925,-0.9705 -0.8724,-1.6421 -0.4797,-0.6717 -1.1665,-1.8179 -1.5259,-2.5474 -0.9428,-1.9133 -0.8327,-2.4052 1.0817,-4.8313 2.0393,-2.5844 5.4954,-7.751 7.5001,-11.212 6.6836,-11.5394 10.2543,-26.3502 10.2918,-42.6902 0.014,-6.1916 -0.3138,-11.1512 -1.4222,-21.504 -0.2511,-2.3446 -0.6286,-6.0107 -0.8388,-8.1469 -0.2102,-2.1362 -0.4642,-4.5234 -0.5643,-5.3051 -0.1004,-0.7815 -0.2787,-2.4013 -0.3968,-3.5996 -0.1181,-1.1984 -0.3302,-2.6905 -0.4713,-3.3156 -0.1411,-0.6253 -0.3476,-1.9042 -0.4588,-2.842 -0.5672,-4.7787 -3.2292,-17.1285 -4.7783,-22.1672 -0.4165,-1.3546 -1.1796,-3.9124 -1.6957,-5.6838 -0.5161,-1.7715 -1.6975,-5.4802 -2.6255,-8.2417 -4.6459,-13.8253 -4.9757,-16.427 -2.6904,-21.2198 2.0776,-4.3574 6.2598,-6.6975 11.403,-6.3802 1.8507,0.1141 3.6912,0.539 8.9047,2.0557 1.6153,0.47 3.4482,0.9897 4.0735,1.155 0.6252,0.1653 2.373,0.7217 3.884,1.2364 4.9437,1.6843 6.8819,2.3162 9.189,2.9957 1.2504,0.3683 2.6145,0.8262 3.0313,1.0174 1.1713,0.5374 2.7637,1.1747 3.5998,1.4405 1.4598,0.4641 5.4471,1.9658 6.6964,2.522 4.255,1.8943 7.767,3.4118 8.1765,3.5329 0.2605,0.077 1.9656,0.8866 3.7893,1.7989 1.8235,0.9123 4.2107,2.0926 5.3049,2.6231 1.0942,0.5304 2.6714,1.3307 3.5051,1.7785 0.8335,0.4478 2.4535,1.3177 3.5997,1.9331 2.5082,1.3467 8.2672,4.7786 10.5669,6.2972 0.9141,0.6037 2.589,1.6943 3.7218,2.4238 1.1329,0.7294 2.6443,1.763 3.3586,2.2968 0.7145,0.5337 1.6835,1.2158 2.1534,1.5157 0.4699,0.2998 2.1752,1.5683 3.7895,2.8188 1.6144,1.2504 3.4399,2.6571 4.0566,3.126 1.8302,1.3913 7.6176,6.4077 9.962,8.6346 1.1986,1.1386 2.4349,2.2909 2.7472,2.5607 0.9207,0.7952 9.8749,9.9437 11.9472,12.2064 3.2265,3.523 6.8834,8.0165 12.5068,15.3683 4.6009,6.0149 5.4863,7.2209 8.1198,11.0588 0.6078,0.8857 1.4643,2.0367 1.9035,2.5577 1.8373,2.1799 1.7315,3.9414 -0.2526,4.2075 -0.7601,0.1024 -0.7601,0.1024 -5.9354,-4.9924 -7.7501,-7.6289 -16.7228,-15.5916 -23.3473,-20.7192 -0.6058,-0.4689 -1.6709,-1.3213 -2.3668,-1.8946 -1.1741,-0.9668 -2.9131,-2.2747 -7.9753,-5.9975 -3.3158,-2.4387 -15.7898,-10.6751 -16.1672,-10.6751 -0.046,0 -0.9668,-0.5405 -2.0468,-1.2011 -1.0801,-0.6606 -3.0295,-1.7804 -4.332,-2.4886 -1.3026,-0.7081 -3.3488,-1.8207 -4.5472,-2.4723 -9.458,-5.1431 -18.9529,-9.5468 -26.1458,-12.1266 -11.9189,-4.2748 -14.3961,-5.0584 -21.4093,-6.7727 -8.4966,-2.0771 -8.9929,-2.1657 -9.9263,-1.7716 -0.8527,0.3599 -0.8888,1.4351 -0.1228,3.6579 0.3803,1.1037 0.5808,1.9703 1.4384,6.218 0.7976,3.9505 1.8022,9.4376 2.1677,11.8414 0.087,0.5732 0.3282,2.0226 0.5356,3.2209 0.573,3.3125 1.3897,9.8038 1.74,13.8308 0.1132,1.3025 0.415,4.5424 0.6706,7.1996 1.2443,12.9373 1.4786,18.1876 1.3605,30.5035 -0.106,11.0649 -0.2174,12.4773 -1.9191,24.346 -1.0104,7.0472 -2.8029,14.646 -5.1398,21.7882 -2.6396,8.0677 -7.4463,15.7878 -11.7695,18.9032 -0.4008,0.2889 -1.3683,0.9881 -2.1498,1.554 -2.3051,1.669 -5.9083,3.3112 -8.7153,3.9722 -1.7095,0.4024 -2.0017,0.3753 -2.4278,-0.2255 z"
id="path21283" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1344.6204,-830.62232 c -6.8773,-2.01541 -12.9376,-5.17715 -21.9342,-11.44321 -11.9734,-8.33945 -21.8594,-22.80374 -21.9023,-32.04531 -0.025,-5.21916 1.4471,-8.79863 5.9642,-14.50954 0.6662,-0.84223 1.8506,-2.36869 2.6322,-3.39215 0.7815,-1.02347 1.6434,-2.12479 1.9151,-2.4474 0.2719,-0.32261 1.168,-1.48177 1.9914,-2.57591 0.8234,-1.09416 3.8768,-4.97341 6.785,-8.62057 2.9084,-3.64716 5.5592,-6.97223 5.8908,-7.38905 1.3392,-1.68346 1.3506,-1.83796 0.207,-2.81492 -5.4037,-4.61652 -13.9573,-19.03987 -17.2069,-29.01484 -0.2037,-0.62524 -0.6723,-1.94674 -1.0413,-2.93668 -0.7402,-1.98575 -1.8645,-5.71704 -2.255,-7.48379 -1.8287,-8.27417 -2.1744,-22.61767 -0.7283,-30.21933 0.1487,-0.78153 0.3973,-2.33949 0.5523,-3.46211 0.4319,-3.12594 1.2016,-5.62552 4.5929,-14.91587 0.7521,-2.06 4.7855,-9.6636 5.9297,-11.1782 2.1853,-2.8926 2.2231,-3.2679 0.5445,-5.3997 -7.4283,-9.4333 -13.6635,-24.3793 -15.2216,-36.4873 -1.3218,-10.271 -1.1235,-23.1421 0.4668,-30.2984 0.9613,-4.3261 1.3428,-5.5729 3.7393,-12.2204 1.3168,-3.6525 4.53,-10.2639 7.0297,-14.4641 0.6414,-1.0779 1.1662,-2.0025 1.1662,-2.0549 0,-0.1073 1.4953,-2.2836 3.0347,-4.4166 6.9984,-9.6974 16.482,-18.5941 25.7084,-24.1172 2.879,-1.7236 4.055,-2.4075 4.1393,-2.4075 0.051,0 0.4349,-0.2167 0.8544,-0.4815 0.4195,-0.2649 1.4623,-0.7866 2.3172,-1.1594 0.8549,-0.3727 1.8954,-0.829 2.3122,-1.014 1.1008,-0.4884 5.5833,-2.148 7.6664,-2.8386 2.3895,-0.7922 6.1267,-1.6365 8.3432,-1.885 0.99,-0.111 2.6526,-0.298 3.6946,-0.4155 3.3891,-0.3824 11.9886,0.011 15.1571,0.6944 0.7293,0.1571 2.4345,0.4601 3.7892,0.6733 4.9466,0.7783 13.676,3.9822 18.7546,6.8835 0.939,0.5364 2.1173,1.1859 2.6184,1.4432 0.5011,0.2573 1.4816,0.9244 2.1789,1.4823 0.6972,0.558 1.6066,1.2319 2.0208,1.4976 8.9372,5.7333 22.8368,21.4683 26.7195,30.2479 0.2352,0.5317 0.9909,2.1002 1.6793,3.4854 2.4129,4.8545 5.4995,14.1279 6.6616,20.0131 2.785,14.1049 1.5763,35.4 -2.5863,45.5637 -0.1034,0.2528 -0.4773,1.3328 -0.8306,2.4002 -1.9693,5.9485 -5.6108,13.0478 -8.9706,17.4881 -3.5901,4.7449 -3.5745,4.7071 -2.5231,6.1377 1.5087,2.0529 5.1523,9.0393 6.1466,11.7859 0.2641,0.7295 0.7089,1.9657 0.9882,2.7472 0.2796,0.7816 0.7036,1.8925 0.9425,2.46876 0.2389,0.57626 0.7887,2.32405 1.2217,3.88399 0.4332,1.55993 0.9061,3.21991 1.0509,3.68884 0.3691,1.19458 0.6598,3.35446 1.2495,9.28367 1.1225,11.28504 0.3564,21.6401 -2.3901,32.30343 -0.7667,2.97684 -2.6423,8.57765 -3.5047,10.46575 -0.2017,0.44174 -0.3669,0.852 -0.3669,0.91169 0,0.36241 -4.4274,9.35514 -5.0324,10.22133 -0.291,0.41682 -0.9529,1.39729 -1.4708,2.17882 -2.6368,3.97864 -3.8477,5.45705 -7.9729,9.73351 -1.8786,1.9476 -1.9011,1.49234 0.2162,4.40819 0.6702,0.92315 1.5315,2.14737 1.9138,2.72049 1.2572,1.88443 4.372,6.30253 6.2112,8.81003 0.9937,1.35467 2.4763,3.44349 3.295,4.64185 0.8187,1.19834 2.5155,3.58558 3.7707,5.30494 3.5394,4.84808 5.8002,8.27771 6.6408,10.07382 4.1125,8.78693 -2.8311,23.35628 -16.3975,34.4058 -0.9895,0.80583 -2.1658,1.76354 -2.6142,2.12826 -6.0837,4.94792 -12.8528,8.95466 -19.8212,11.73254 -8.2134,3.27414 -11.0944,3.55091 -11.4915,1.10397 -0.2547,-1.56961 0.017,-2.05948 4.8305,-8.66833 1.4037,-1.92777 3.0215,-4.18712 3.5952,-5.02076 0.5737,-0.83363 1.5713,-2.28303 2.2168,-3.22087 1.0612,-1.54145 1.7115,-2.60302 4.6429,-7.57851 2.9165,-4.95017 5.4898,-11.05328 5.4898,-13.02015 0,-1.24229 -1.2524,-3.30859 -4.7051,-7.76369 -1.8358,-2.36875 -3.3099,-4.36196 -8.6593,-11.70906 -0.645,-0.88573 -2.4844,-3.55999 -4.0877,-5.94276 -3.5111,-5.21787 -2.6716,-4.99024 -9.4518,-2.56243 -1.1251,0.40291 -5.8005,1.2988 -8.2415,1.57925 -1.4589,0.16762 -3.2231,0.38776 -3.9205,0.48922 -2.7564,0.40097 -8.2369,0.16605 -13.6049,-0.58319 -2.2703,-0.31689 -6.6673,-1.46279 -9.9467,-2.59221 -4.2126,-1.45078 -3.9885,-1.45039 -5.0173,-0.009 -0.4669,0.65438 -1.49,2.01033 -2.2735,3.01322 -1.4235,1.82216 -3.3121,4.32005 -4.5369,6.00074 -0.3573,0.49011 -1.9772,2.55386 -3.5999,4.58611 -1.6227,2.03227 -3.1891,4.0145 -3.481,4.40497 -0.2918,0.39047 -0.9608,1.2002 -1.4865,1.7994 -0.8925,1.01738 -3.5659,4.47412 -4.7634,6.1593 -2.8314,3.98464 -2.114,7.76744 3.4537,18.21334 1.3598,2.55114 3.963,6.50495 8.4339,12.80951 5.5864,7.87782 6.0591,8.78903 5.3102,10.2372 -0.6582,1.27276 -1.589,1.36792 -4.6386,0.47424 z"
id="path21269" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1388.7652,-1007.5996 c -5.8227,-2.6259 -9.1991,-5.437 -11.9327,-9.9347 -0.3484,-0.5731 -1.2023,-1.9799 -1.8977,-3.126 -1.3115,-2.162 -4.3598,-8.3758 -5.2191,-10.6392 -1.282,-3.3764 -3.4016,-10.1595 -3.8827,-12.4249 -0.2051,-0.9655 -0.4216,-1.8835 -0.4812,-2.0398 -0.1639,-0.4303 -0.9986,-4.5519 -1.4196,-7.0101 -0.8001,-4.6732 -1.1514,-7.7036 -1.8892,-16.2938 -0.7911,-9.212 -0.2779,-27.3932 1.1474,-40.6399 0.112,-1.042 0.3283,-3.1719 0.4807,-4.733 0.1523,-1.5612 0.4449,-3.9485 0.6504,-5.305 0.2054,-1.3565 0.4598,-3.0633 0.5655,-3.7927 0.4804,-3.3151 1.5541,-9.6808 1.9816,-11.7468 0.7494,-3.623 1.428,-6.7493 1.6226,-7.4756 0.099,-0.3691 0.3904,-1.5664 0.6479,-2.6606 0.2574,-1.0941 0.7252,-3.055 1.0394,-4.3577 1.2841,-5.3225 1.5878,-5.1937 -7.3698,-3.1246 -10.1381,2.3418 -14.1671,3.4752 -20.5567,5.7826 -1.7715,0.6397 -4.1459,1.4961 -5.2765,1.903 -1.1305,0.4069 -2.7504,1.0467 -3.5997,1.4217 -0.8494,0.375 -1.8001,0.7918 -2.1127,0.9265 -1.5546,0.6693 -8.3608,3.7741 -8.5258,3.8894 -0.1042,0.073 -1.0421,0.5842 -2.0841,1.1366 -1.0421,0.5523 -2.0652,1.1097 -2.2735,1.2387 -0.2085,0.1289 -1.4448,0.7837 -2.7473,1.455 -1.3025,0.6713 -2.7093,1.4174 -3.1262,1.6581 -0.4167,0.2406 -1.7383,0.9519 -2.9366,1.5808 -1.1984,0.6289 -2.733,1.4821 -3.4103,1.8961 -2.6246,1.6041 -3.9572,2.3753 -5.8984,3.4146 -1.1078,0.593 -3.0877,1.7803 -4.3999,2.6385 -1.312,0.8582 -2.7928,1.8089 -3.2906,2.1126 -11.4464,6.9844 -29.4494,21.4049 -40.9311,32.7859 -5.9123,5.8603 -6.2292,6.0493 -7.4275,4.4286 -0.7969,-1.0778 -0.6741,-1.3984 2.0205,-5.2738 10.6149,-15.2674 32.1009,-37.4481 48.1056,-49.6614 1.1449,-0.8736 2.3333,-1.7822 2.6411,-2.0191 3.1702,-2.4402 4.511,-3.4358 5.4173,-4.0226 0.5877,-0.3804 1.3976,-0.948 1.8,-1.2612 0.4022,-0.3134 1.6693,-1.2092 2.8156,-1.9909 1.1462,-0.7817 2.894,-1.984 3.8839,-2.672 0.99,-0.688 2.4394,-1.6551 3.2209,-2.1492 0.7815,-0.4942 2.3172,-1.47 3.4125,-2.1685 1.0952,-0.6985 2.502,-1.5457 3.126,-1.8826 1.9664,-1.0615 3.1618,-1.7264 5.1135,-2.844 4.9429,-2.8307 15.9289,-7.9772 21.883,-10.2514 1.6151,-0.6169 3.1072,-1.2028 3.3156,-1.3019 1.451,-0.6899 6.0037,-2.3879 8.6205,-3.215 4.7239,-1.4933 4.8035,-1.5193 5.2102,-1.7075 1.2028,-0.5562 12.0225,-3.8689 15.0624,-4.6116 7.9785,-1.9496 12.6945,-0.5743 16.2248,4.7315 2.8387,4.266 2.9057,7.8163 0.2694,14.2737 -2.741,6.7145 -6.0927,16.1664 -6.8525,19.3252 -0.2131,0.8858 -0.5836,2.2499 -0.8235,3.0314 -0.2399,0.7815 -0.584,1.9752 -0.7646,2.6524 -0.1805,0.6774 -0.4,1.4447 -0.4875,1.7052 -0.1494,0.4448 -1.3994,5.5403 -1.9752,8.0522 -0.5596,2.4409 -1.2398,5.7822 -1.6007,7.8627 -0.2079,1.1984 -0.5029,2.8183 -0.6556,3.5998 -0.1527,0.7815 -0.4557,2.572 -0.6734,3.9787 -0.2178,1.4068 -0.4754,3.0694 -0.5725,3.6946 -0.097,0.6252 -0.223,1.4352 -0.2795,1.7999 -2.4243,15.6279 -2.8728,36.4364 -1.0455,48.5025 1.9607,12.9468 8.2616,27.6355 16.2343,37.8451 2.9208,3.7401 2.9562,3.9441 1.1076,6.3659 -0.635,0.8319 -1.6846,2.499 -3.4769,5.523 -1.7723,2.9903 -1.6432,2.9649 -5.7239,1.1246 z"
id="path21281" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1571.8105,-906.44907 c -1.0547,-0.65454 -1.3054,-1.68463 -0.94,-3.86175 0.2379,-1.41654 0.7097,-5.88837 1.1581,-10.97628 0.8133,-9.22935 1.067,-11.27594 2.4537,-19.79887 0.1013,-0.62523 0.3166,-1.94673 0.4777,-2.93667 0.4792,-2.9466 0.8115,-4.75966 1.236,-6.74439 0.2208,-1.0319 0.5684,-2.68613 0.7723,-3.67607 0.6246,-3.03085 2.6171,-10.75914 3.4192,-13.26241 1.6799,-5.24257 3.4547,-10.55742 3.7646,-11.27304 0.3425,-0.79122 2.0249,-5.06696 3.4713,-8.82247 0.4641,-1.2052 1.1407,-2.78248 1.5034,-3.50506 0.3627,-0.72259 1.1739,-2.55321 1.8027,-4.06804 0.6286,-1.51484 1.7153,-3.9447 2.4146,-5.39968 0.6993,-1.4551 1.7363,-3.6685 2.3045,-4.919 0.5682,-1.2504 1.4429,-3.0409 1.9438,-3.9787 0.5009,-0.9379 1.5935,-3.0267 2.4277,-4.6419 3.0705,-5.9442 6.3383,-11.2849 11.7084,-19.1358 1.8857,-2.7567 3.0674,-4.3946 4.7246,-6.5481 0.8336,-1.0834 1.8141,-2.3719 2.1788,-2.8635 4.1072,-5.5347 16.4116,-19.086 24.9999,-27.5336 12.9724,-12.7598 23.566,-21.8905 31.9564,-27.5434 0.6378,-0.4298 2.0871,-1.4313 3.2209,-2.2257 10.1055,-7.0808 16.533,-8.3386 21.8208,-4.2698 3.6021,2.7718 4.4487,4.9992 4.3681,11.4929 -0.1413,11.3874 0.1722,15.6696 1.7267,23.588 1.7288,8.8065 2.063,10.3445 2.4948,11.4807 0.1949,0.5133 0.3546,1.1347 0.3546,1.381 0,0.2464 0.1342,0.8209 0.2984,1.2769 0.1641,0.456 0.4973,1.4684 0.7404,2.2499 1.2782,4.1093 2.5916,7.5374 4.5583,11.8984 1.4749,3.2706 2.0342,4.4268 3.5472,7.3322 0.4882,0.9378 1.5167,3.0266 2.2853,4.6419 1.5516,3.2605 4.8531,9.2879 6.4109,11.7043 0.5561,0.8625 1.4799,2.3487 2.053,3.3027 3.7694,6.2741 13.5463,12.8354 22.5461,15.13059 3.196,0.81504 4.3536,1.55881 3.9753,2.55393 -0.1003,0.26379 -0.487,1.56324 -0.8591,2.88767 -0.3722,1.32442 -0.9655,3.26062 -1.3184,4.30266 -0.58,1.71309 -1.0603,3.36793 -1.6757,5.77369 -0.5482,2.14332 -6.7881,1.27333 -15.422,-2.1502 -8.0086,-3.17554 -17.6559,-12.92694 -27.2498,-27.54384 -4.4414,-6.7667 -7.3082,-11.5271 -9.2697,-15.392 -1.5617,-3.0775 -4.6293,-9.6326 -5.4654,-11.6792 -0.4625,-1.132 -1.1114,-2.6974 -1.4421,-3.4791 -2.766,-6.5376 -6.2829,-18.1636 -7.3074,-24.1564 -0.3002,-1.7559 -0.5918,-3.1052 -1.0543,-4.8788 -0.2984,-1.1444 -0.3933,-1.1092 -5.381,1.9983 -0.8336,0.5192 -1.8263,1.2222 -2.2059,1.5621 -0.3797,0.3397 -1.1469,0.914 -1.7051,1.2762 -0.5582,0.362 -1.3134,0.9325 -1.678,1.2676 -0.3648,0.3351 -1.6383,1.4328 -2.8301,2.4392 -2.658,2.2445 -5.3855,4.6523 -7.0221,6.1986 -0.6772,0.6401 -2.7217,2.5194 -4.5431,4.1761 -21.9692,19.9833 -41.2206,42.1321 -50.6218,58.24075 -0.5777,0.98994 -1.7789,3.05012 -2.6692,4.57818 -0.8904,1.52806 -2.6622,4.89576 -3.9375,7.48379 -1.2752,2.58803 -3.0553,6.19751 -3.9556,8.02109 -0.9004,1.82358 -2.0531,4.25344 -2.5616,5.3997 -0.5084,1.14624 -1.5101,3.30344 -2.2259,4.79376 -0.7159,1.49033 -1.6053,3.45127 -1.9763,4.35765 -0.615,1.50217 -0.9401,2.24076 -1.9767,4.48991 -0.5089,1.10425 -1.6261,3.89962 -2.3852,5.96809 -0.3633,0.98994 -0.945,2.52459 -1.2927,3.41033 -0.3476,0.88574 -0.9309,2.37776 -1.2961,3.3156 -0.8338,2.14107 -4.9012,14.44931 -5.4472,16.48327 -0.9205,3.42976 -3.2479,13.27335 -3.494,14.77811 -0.9547,5.83668 -1.8912,7.28064 -3.9095,6.028 z"
id="path21279" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1051.2372,-905.15276 c -1.2128,-0.7415 -1.3505,-1.20128 -3.0447,-10.16625 -0.256,-1.35467 -0.5894,-2.97457 -0.741,-3.5998 -0.1515,-0.62523 -0.5832,-2.45829 -0.9591,-4.07345 -0.6624,-2.84563 -1.7035,-6.50494 -3.0185,-10.60993 -0.3505,-1.09414 -1.035,-3.26823 -1.5211,-4.8313 -2.3498,-7.55702 -3.8122,-11.08546 -9.1874,-22.16716 -2.5982,-5.35645 -3.5948,-7.47553 -4.5895,-9.75733 -0.5224,-1.19836 -1.4012,-3.07404 -1.953,-4.16819 -0.5518,-1.09415 -1.5178,-3.05509 -2.1465,-4.35765 -0.6289,-1.30256 -1.8404,-3.60453 -2.6921,-5.11549 -0.8519,-1.51097 -2.3639,-4.19661 -3.3602,-5.96809 -5.4984,-9.77688 -8.1194,-14.0045 -11.6615,-18.8096 -3.7994,-5.154 -8.4351,-10.9504 -10.7163,-13.3991 -4.9116,-5.2723 -6.4436,-6.9063 -8.2561,-8.8064 -3.3825,-3.5455 -11.8124,-11.9301 -16.2939,-16.2061 -2.2914,-2.1864 -5.0623,-4.8313 -6.1575,-5.8774 -3.6359,-3.4732 -11.5809,-10.0951 -16.0115,-13.3453 -1.0421,-0.7644 -2.2818,-1.7105 -2.755,-2.1025 -0.8091,-0.6703 -4.9304,-3.3655 -6.4716,-4.2322 -1.3351,-0.7508 -2.1,0.4074 -2.8046,4.2462 -0.4155,2.2637 -1.4048,6.6847 -1.7172,7.6733 -0.099,0.3126 -0.4361,1.6768 -0.7495,3.0314 -0.3136,1.3547 -0.7805,3.1024 -1.0379,3.8839 -0.2573,0.7816 -0.8463,2.572 -1.3089,3.9788 -2.5234,7.6721 -5.3912,14.0913 -11.3421,25.388 -0.5214,0.9899 -1.4266,2.5913 -2.0114,3.5585 -0.5847,0.9673 -1.2283,2.033 -1.4302,2.3683 -0.6609,1.098 -2.8252,4.3842 -3.3339,5.0621 -0.2737,0.3647 -1.1661,1.6009 -1.9832,2.7472 -1.3797,1.9352 -3.5465,4.6994 -7.4859,9.5499 -5.7859,7.12376 -13.6661,13.4112 -20.0807,16.02195 -0.6773,0.27567 -1.8284,0.78419 -2.5578,1.13005 -0.7295,0.34584 -2.4345,0.9869 -3.7893,1.42456 -1.3546,0.43764 -2.6761,0.88618 -2.9366,0.99673 -2.5596,1.08615 -4.7828,0.35241 -5.1012,-1.68359 -0.1276,-0.81576 -0.3585,-1.95212 -0.5131,-2.52524 -0.1547,-0.57313 -0.4434,-1.63886 -0.6415,-2.36829 -2.6894,-9.89996 -2.675,-9.06013 -0.1708,-10.02123 3.9174,-1.50353 5.4635,-2.18474 8.4457,-3.72104 1.7878,-0.921 3.6299,-1.9572 4.0934,-2.3026 0.4636,-0.3453 1.6305,-1.1766 2.593,-1.8474 6.1024,-4.2521 11.0526,-10.4225 16.2206,-20.2192 0.5962,-1.1301 1.5781,-2.9499 2.182,-4.044 0.604,-1.0942 1.5389,-2.9698 2.0775,-4.1682 0.5386,-1.1984 1.5227,-3.2973 2.1868,-4.6643 0.6639,-1.3671 1.4526,-3.1574 1.7524,-3.9788 0.2999,-0.8212 0.9905,-2.6442 1.5346,-4.0509 1.1175,-2.8893 2.4311,-7.0308 3.0345,-9.5679 0.2232,-0.9379 0.6529,-2.6003 0.955,-3.6946 0.6533,-2.3661 1.7288,-7.6513 2.2556,-11.0834 0.7297,-4.755 1.3694,-10.6082 2.0191,-18.4727 0.4939,-5.9779 0.6948,-7.1517 1.5301,-8.939 2.5612,-5.4798 7.7868,-7.9638 13.906,-6.6103 1.5611,0.3453 8.495,3.9855 9.7521,5.1198 0.2085,0.188 1.7005,1.2135 3.3157,2.2789 1.6152,1.0655 3.1638,2.096 3.4413,2.2903 0.2776,0.1941 1.4712,1.0065 2.6525,1.8053 5.8384,3.9476 14.7552,11.1304 20.3363,16.3816 0.6252,0.5883 1.5204,1.4017 1.9893,1.8074 4.5567,3.9431 17.3336,17.3297 23.1693,24.275 8.5254,10.1465 13.9509,17.4905 18.5143,25.0611 0.6594,1.0941 1.5719,2.5712 2.0276,3.2825 0.731,1.1411 1.8009,3.044 2.6806,4.7677 0.1591,0.3116 0.4851,0.8552 0.7246,1.2082 0.4257,0.6273 5.0629,9.9065 7.3031,14.6139 0.6198,1.3025 1.7128,3.57013 2.4288,5.03911 2.0609,4.22846 5.1798,11.371 6.1555,14.0966 0.786,2.19598 0.9256,2.56314 2.053,5.39969 0.8712,2.19209 2.8402,7.95218 4.6628,13.64133 0.5074,1.58381 1.298,4.57484 1.8028,6.82066 0.2342,1.04205 0.7233,3.04562 1.0868,4.45238 1.5578,6.02916 2.2082,9.40886 3.4451,17.90424 0.6436,4.42041 1.2381,10.03592 1.5244,14.39919 0.3392,5.17256 0.7692,9.79107 1.1527,12.38379 0.2983,2.0167 0.1909,2.42655 -0.8699,3.31907 -0.6998,0.58886 -0.8569,0.60328 -1.6027,0.14728 z"
id="path21277" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1524.6154,-780.78748 c -0.8444,-0.46733 -4.9487,-8.46179 -7.1559,-13.9384 -0.735,-1.82358 -1.5662,-3.82715 -1.8471,-4.45239 -0.281,-0.62522 -0.7864,-1.86147 -1.1229,-2.7472 -0.3367,-0.88574 -1.0594,-2.76143 -1.6061,-4.16819 -2.6659,-6.86063 -6.3122,-18.68126 -7.393,-23.96706 -0.085,-0.41681 -0.4684,-2.20723 -0.8515,-3.97872 -0.3832,-1.77147 -0.8549,-4.07345 -1.0484,-5.11549 -0.1934,-1.04205 -0.5233,-2.74722 -0.7329,-3.78926 -0.4116,-2.04514 -1.1927,-7.49489 -1.5345,-10.70465 -0.1166,-1.09415 -0.2907,-2.67144 -0.3872,-3.50506 -1.5314,-13.23132 -2.0562,-45.40144 -0.855,-52.40895 0.7139,-4.16531 3.4229,-10.44385 7.3068,-16.93447 0.9977,-1.66728 2.2904,-3.84137 2.8726,-4.83132 4.2349,-7.19922 14.5483,-21.51103 19.64,-27.25427 0.6151,-0.69362 1.8315,-2.12942 2.7031,-3.19066 1.4483,-1.76311 3.2212,-3.82542 6.307,-7.33691 0.6333,-0.72063 1.4551,-1.65847 1.8263,-2.08408 8.5053,-9.75158 26.5817,-28.05284 32.7912,-33.19894 1.5106,-1.2519 4.2382,-3.2887 4.4042,-3.2887 0.043,0 0.6625,-0.3553 1.3766,-0.7896 6.5868,-4.0062 12.6237,-1.4734 16.9602,7.1156 6.1139,12.10981 22.6254,24.42593 38.0408,28.37518 4.6331,1.18692 12.7273,1.37594 18.1475,0.42379 3.5814,-0.62914 3.6764,-0.50378 3.8755,5.11353 0.082,2.30482 0.237,4.6595 0.3451,5.23262 0.985,5.22329 0.4784,5.83008 -6.0051,7.1936 -9.8694,2.0756 -18.3529,1.41914 -29.8049,-2.30637 -4.7285,-1.53823 -13.0235,-5.40727 -16.3323,-7.61777 -5.8468,-3.90622 -8.6799,-6.202 -14.3829,-11.65513 -7.092,-6.78131 -6.8261,-6.89037 -22.91,9.39259 -4.1669,4.21838 -8.2028,8.45933 -11.063,11.62525 -4.2273,4.67879 -14.137,16.6436 -16.8861,20.38803 -0.5267,0.71747 -2.441,3.26545 -4.254,5.66215 -2.7303,3.60937 -7.7461,10.91483 -11.9675,17.43059 -4.9454,7.63321 -7.4094,14.57972 -7.9173,22.32032 -0.5833,8.88979 -0.4109,35.28451 0.2749,42.09705 0.1469,1.45886 0.3577,4.05925 0.4685,5.77862 0.1899,2.94863 1.1437,12.08686 1.5423,14.77811 0.1004,0.67733 0.3497,2.42513 0.5541,3.88399 0.5027,3.58842 1.0897,7.22117 1.6993,10.51519 0.2025,1.09415 0.5476,2.96983 0.7669,4.16818 0.2195,1.19836 0.5981,3.03141 0.8416,4.07345 0.2435,1.04205 0.7998,3.42928 1.2361,5.30496 0.4363,1.87569 0.8719,3.66611 0.968,3.97873 0.096,0.31261 0.4355,1.50623 0.7544,2.65247 0.7699,2.76789 2.4393,7.86098 2.7325,8.33638 1.1206,1.81751 -0.6567,4.37589 -2.3779,3.42321 z"
id="path21267" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1097.9379,-779.80916 c -1.5217,-0.76164 -1.5219,-0.74568 0.099,-6.41702 2.0228,-7.07604 3.1431,-12.12399 4.2621,-19.20436 3.7688,-23.8461 4.9839,-39.66869 5.2306,-68.11191 0.1888,-21.77417 0.081,-22.38837 -6.0881,-34.54459 -4.493,-8.85397 -9.3296,-16.42058 -17.218,-26.93612 -2.8761,-3.8341 -8.9588,-11.40904 -11.4715,-14.28588 -1.3108,-1.50077 -2.98,-3.42996 -3.7094,-4.28712 -6.9198,-8.13138 -22.334,-24.34391 -24.94,-26.2317 -0.7498,-0.54319 -0.8815,-0.57166 -2.643,-0.57166 -2.616,0 -2.0764,-0.32663 -8.2068,4.96805 -1.0524,0.90885 -2.457,2.05983 -3.1216,2.55775 -0.6645,0.49791 -1.9325,1.48954 -2.8178,2.20361 -0.8852,0.71408 -2.6009,1.94393 -3.8125,2.73302 -1.2117,0.78909 -2.923,1.90263 -3.8031,2.47451 -1.3652,0.88717 -4.8952,2.76907 -9.3888,5.0053 -6.7795,3.37386 -12.2222,5.1714 -20.5567,6.78898 -2.9982,0.58192 -11.483,0.46369 -14.5886,-0.20329 -4.9485,-1.06274 -6.1602,-1.36976 -8.088,-2.04933 -4.2288,-1.49071 -3.9708,-0.90056 -3.7243,-8.52251 0.2764,-8.54566 0.028,-8.21009 5.4652,-7.37133 8.5133,1.31317 21.891,-0.86397 32.3035,-5.25721 7.8248,-3.30145 22.1897,-15.31752 26.6041,-22.25404 8.0597,-12.66469 13.6596,-13.81619 23.7486,-4.88339 0.6085,0.5388 1.5708,1.3633 2.1385,1.8322 0.5676,0.4689 2.3633,2.1741 3.9903,3.78924 1.6271,1.61517 3.8011,3.74663 4.8313,4.73658 1.8519,1.77949 7.212,7.363 8.8135,9.18089 0.8257,0.9371 1.5732,1.76305 4.8185,5.3235 3.3087,3.63027 5.4951,6.09566 6.9979,7.89087 3.4173,4.08236 6.8911,8.40952 9.475,11.80279 1.0316,1.35466 2.3484,3.07024 2.9262,3.81241 0.5779,0.74217 1.5622,2.05768 2.1874,2.92334 0.6252,0.86567 1.5545,2.13412 2.0651,2.81879 0.9338,1.25213 1.2132,1.6624 4.4851,6.58414 1.7856,2.6859 3.5028,5.38793 5.5773,8.77568 0.6062,0.98995 1.4196,2.26883 1.8076,2.84195 7.6515,11.30182 10.6721,18.58023 11.7548,28.3247 0.8511,7.65988 0.6759,22.00663 -0.4835,39.59775 -0.3185,4.83145 -0.7768,10.37169 -1.0503,12.69401 -0.8074,6.85862 -1.2343,9.74693 -2.4638,16.67274 -2.2601,12.73024 -4.7741,21.89077 -9.0472,32.96654 -2.2607,5.85946 -2.5652,6.57714 -5.063,11.93616 -0.1457,0.31262 -0.5649,1.22341 -0.9315,2.024 -0.7166,1.56464 -2.5404,4.84492 -3.4434,6.19326 -0.3051,0.45553 -0.6802,1.02595 -0.8336,1.26762 -0.2173,0.34249 -0.9448,0.8518 -1.1984,0.83889 -0.022,-10e-4 -0.4212,-0.19359 -0.8891,-0.42781 z"
id="path21265" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1372.6609,-723.60939 c -1.855,-1.08537 -4.8912,-2.66017 -6.0674,-3.14684 -0.3672,-0.15195 -1.305,-0.64271 -2.0841,-1.09057 -0.779,-0.44786 -1.7574,-1.00342 -2.1743,-1.23458 -14.7165,-8.16137 -26.5442,-17.60623 -38.668,-30.87807 -1.0471,-1.14624 -2.5693,-2.80878 -3.3827,-3.69452 -6.0598,-6.59929 -15.1394,-19.26533 -19.6251,-27.37738 -0.3458,-0.62524 -0.9971,-1.75268 -1.4472,-2.50545 -1.1856,-1.98224 -5.1055,-9.33118 -6.041,-11.32535 -4.9318,-10.51271 -5.3147,-11.36642 -6.799,-15.15703 -0.6936,-1.77148 -1.3829,-3.51928 -1.5316,-3.88399 -4.1146,-10.08764 -8.8787,-28.0974 -11.8529,-44.80798 -0.1113,-0.62522 -0.2824,-1.52044 -0.3802,-1.98935 -0.2293,-1.09929 -1.5686,-8.81206 -1.9575,-11.27305 -0.9185,-5.81186 -1.4482,-9.36218 -1.7234,-11.55054 -0.1697,-1.35097 -0.3385,-2.50452 -0.3748,-2.56345 -0.6387,-1.03341 -1.0457,-17.89571 -0.4939,-20.46299 1.4224,-6.61767 6.253,-10.04152 14.1674,-10.04152 5.877,0 13.7905,-0.52996 18.5345,-1.24122 8.7162,-1.30683 14.3143,-2.50047 20.854,-4.44658 2.0938,-0.62307 3.8675,-1.13287 3.9415,-1.13287 0.3724,0 6.2057,-2.27581 8.3605,-3.26173 1.3547,-0.61983 3.1451,-1.4396 3.9787,-1.82169 3.0726,-1.40835 9.8708,-5.08706 13.7795,-7.45654 3.1123,-1.88663 3.5776,-1.96379 4.2304,-0.70151 0.3327,0.64336 1.4487,3.19759 1.9891,4.55215 0.1927,0.48321 0.6158,1.42104 0.9401,2.08409 0.5875,1.20113 0.9563,2.03753 2.0651,4.68286 1.2859,3.06795 1.1036,3.3249 -6.1075,8.61006 -9.0907,6.66273 -18.42,11.26102 -29.117,14.35129 -0.6774,0.19567 -1.9562,0.57649 -2.8421,0.84625 -6.3059,1.92056 -12.5797,3.58867 -15.3463,4.08041 -1.0943,0.19447 -3.4814,0.66044 -5.3051,1.03548 -2.6726,0.54967 -3.9401,0.70737 -6.5365,0.81327 -3.6681,0.14963 -3.9083,1.71404 -2.2743,14.81475 0.2952,2.36737 0.4021,3.21795 0.9405,7.48378 0.4065,3.22214 0.9662,7.17231 1.4228,10.04154 0.2156,1.35466 0.4788,3.14509 0.5851,3.97872 0.1062,0.83364 0.2305,1.64359 0.2763,1.79991 0.083,0.28256 0.2587,1.38253 0.7418,4.64184 0.1389,0.93783 0.4251,2.55775 0.6358,3.59979 0.2106,1.04204 0.4899,2.53406 0.6203,3.3156 0.1306,0.78154 0.5298,2.82773 0.8872,4.54711 0.3574,1.71937 0.7294,3.59506 0.8267,4.16818 0.097,0.57314 0.3364,1.72411 0.5312,2.55775 0.1948,0.83364 0.5018,2.19777 0.6823,3.03141 0.4337,2.00305 1.7379,6.74135 2.2162,8.05217 0.3996,1.09491 1.1859,3.73481 1.4952,5.02076 0.1723,0.71552 0.5158,1.84549 1.4979,4.92605 0.2326,0.72942 0.49,1.58202 0.572,1.89462 0.082,0.31262 0.6755,2.06041 1.3187,3.88399 0.6432,1.82358 1.3351,3.84353 1.5376,4.48878 0.2024,0.64525 0.7493,2.09463 1.2155,3.22087 0.466,1.12623 0.8477,2.10569 0.8483,2.17657 6e-4,0.0709 0.2612,0.71032 0.5791,1.42097 0.5705,1.27492 0.7175,1.61726 1.897,4.41823 0.3291,0.78153 1.1807,2.57196 1.8924,3.97872 0.7116,1.40676 1.5829,3.19719 1.936,3.97872 0.3532,0.78154 1.0113,2.06042 1.4627,2.84195 0.4513,0.78153 1.4695,2.57196 2.2628,3.97872 6.593,11.69136 15.602,23.93781 25.7851,35.05063 9.5484,10.42028 15.4447,16.05809 25.8561,24.72247 2.2491,1.8717 2.3682,2.03268 2.3682,3.19815 0,2.09239 -0.9685,2.29585 -3.5997,0.75619 z"
id="path21263" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1254.2051,-722.65362 c -1.6526,-0.72227 -1.2581,-2.7138 0.8463,-4.27161 5.6438,-4.17796 13.9202,-11.64804 22.097,-19.94413 8.5309,-8.65531 18.7748,-20.97388 23.3325,-28.05765 0.4022,-0.62522 1.4323,-2.20251 2.2888,-3.50507 2.1285,-3.23636 5.2013,-8.11401 5.2013,-8.25616 0,-0.064 0.1595,-0.34274 0.3544,-0.61934 0.6114,-0.86732 4.4924,-8.53149 5.6755,-11.20756 0.6218,-1.40676 1.558,-3.46435 2.0803,-4.5724 0.9365,-1.98682 1.31,-2.90177 3.7029,-9.06893 1.4092,-3.63197 1.711,-4.49825 2.772,-7.95744 0.4314,-1.40676 0.8575,-2.7709 0.9467,-3.03141 0.2126,-0.62077 1.1774,-3.88455 1.68,-5.68389 0.4577,-1.6378 1.3455,-5.27987 1.8244,-7.48379 0.1811,-0.83363 0.584,-2.58143 0.8952,-3.88398 1.3142,-5.50072 2.6887,-12.23464 3.6546,-17.90425 0.2486,-1.45886 0.7154,-4.05923 1.0375,-5.77861 0.6987,-3.73001 1.1776,-6.37065 1.8043,-9.94681 0.2556,-1.45886 0.5553,-3.16403 0.6659,-3.78925 0.5375,-3.03905 2.0932,-12.96551 2.3606,-15.06231 1.6926,-13.27309 1.856,-12.4299 -2.4573,-12.68298 -3.3912,-0.19897 -5.997,-0.48133 -7.1034,-0.76973 -0.3648,-0.095 -2.3778,-0.51951 -4.4736,-0.94319 -19.2696,-3.89569 -36.4831,-10.59305 -48.1024,-18.7155 -0.7294,-0.50991 -2.5798,-1.79863 -4.112,-2.86384 -4.8618,-3.37978 -4.8587,-3.32882 -0.7619,-12.52725 3.0548,-6.85891 3.0497,-6.85713 9.1736,-3.16868 2.9739,1.79121 4.4591,2.61223 9.1522,5.05926 4.0054,2.08844 10.8696,4.92111 15.1571,6.25491 1.042,0.32417 2.5587,0.80325 3.3706,1.06461 3.549,1.14264 11.2705,2.95702 14.7569,3.46754 0.6588,0.0965 2.3487,0.34929 3.7554,0.56186 5.7738,0.8724 12.2027,1.30926 20.9356,1.42259 14.5004,0.18817 18.0672,5.91463 16.1219,25.88297 -1.3253,13.60387 -2.1984,19.80697 -3.9998,28.41943 -0.2506,1.19835 -0.7167,3.58558 -1.0356,5.30497 -0.319,1.71937 -0.8816,4.57552 -1.2504,6.34701 -0.7571,3.63751 -0.8708,4.12471 -3.582,15.34649 -0.9586,3.96733 -2.0322,7.74735 -3.6324,12.78873 -1.5436,4.86313 -1.8818,5.83664 -3.2246,9.28369 -0.6698,1.71938 -1.3444,3.48857 -1.4992,3.93155 -0.84,2.40519 -4.0902,9.73005 -5.7376,12.93065 -0.295,0.57312 -1.2563,2.44881 -2.136,4.16818 -0.8798,1.71938 -2.0745,3.93609 -2.6548,4.92604 -0.5803,0.98994 -1.7672,3.03614 -2.6374,4.5471 -3.5102,6.09423 -11.617,17.64916 -15.6188,22.26189 -0.5424,0.62524 -1.4675,1.72409 -2.0558,2.44192 -0.998,1.21775 -2.4563,2.85582 -5.4124,6.07945 -1.8797,2.04973 -7.949,8.23829 -9.0164,9.19341 -0.524,0.46893 -1.629,1.44939 -2.4554,2.17883 -0.8264,0.72943 -2.0431,1.81259 -2.7038,2.40701 -3.2543,2.92779 -8.4457,6.9981 -12.1927,9.55965 -1.1949,0.81686 -3.0218,2.06744 -4.0595,2.77906 -4.5775,3.13877 -8.3453,5.40371 -17.6123,10.58723 -5.5026,3.07787 -5.136,2.92803 -6.116,2.49973 z"
id="path21251" /><path
style="display:inline;fill:#f0b116;fill-opacity:1;stroke-width:0.71608"
d="m -1307.2962,-907.92432 c 5.8064,-0.59544 7.8147,-1.00764 12.5522,-2.57613 4.2944,-1.42185 8.87,-3.62563 14.2567,-6.86678 6.4938,-3.90724 13.6267,-11.43595 18.3424,-19.36028 0.1237,-0.20796 0.3647,-0.59162 0.5354,-0.85259 1.1448,-1.7502 3.829,-7.5377 5.0538,-10.89729 0.8322,-2.28253 1.2356,-3.72692 2.5122,-8.99712 1.8832,-7.77515 2.0463,-19.63405 0.3795,-27.61476 -1.9465,-9.32015 -4.8114,-17.20193 -7.8092,-21.48533 -0.1936,-0.2767 -0.352,-0.5489 -0.352,-0.605 0,-0.3455 -3.5929,-5.3288 -4.9875,-6.9177 -10.2926,-11.7266 -23.8495,-19.4263 -37.6416,-21.379 -5.448,-0.7713 -19.9353,-0.2318 -21.6137,0.805 -0.06,0.037 -0.953,0.2921 -1.9838,0.5664 -2.9979,0.7975 -7.2761,2.2951 -8.6112,3.0141 -0.6774,0.3648 -1.9561,1.0129 -2.842,1.4403 -2.5459,1.2283 -3.0101,1.4882 -5.0207,2.8107 -3.9566,2.6025 -12.4444,10.2311 -14.5716,13.0966 -0.4262,0.5741 -1.3347,1.7677 -2.0188,2.6524 -1.1516,1.4892 -3.398,4.9105 -3.398,5.1752 0,0.064 -0.325,0.6182 -0.7223,1.2329 -0.3973,0.6146 -0.8728,1.5341 -1.0569,2.0431 -0.1839,0.509 -0.7474,1.7355 -1.2522,2.72541 -1.9703,3.86443 -3.4839,8.25685 -4.4396,12.88347 -0.269,1.30256 -0.5862,2.83721 -0.705,3.41033 -0.8952,4.32026 -0.7684,16.13844 0.2338,21.78823 0.2125,1.19836 0.5122,2.90352 0.6659,3.78926 1.7048,9.82062 7.4186,22.39482 13.2584,29.17729 5.4937,6.38047 9.8672,10.1928 14.1301,12.31723 0.8698,0.43343 2.0598,1.0782 2.6446,1.43282 1.6125,0.97786 7.6407,3.87 8.0664,3.87 0.3801,0 1.8456,0.40791 4.4169,1.22939 0.7816,0.24968 2.0179,0.55478 2.7473,0.67801 0.7294,0.12324 1.4806,0.3044 1.6692,0.40259 1.1835,0.61617 11.7551,1.62025 14.2457,1.35304 0.2084,-0.0223 1.7003,-0.17617 3.3156,-0.34179 z"
id="path21272" /><path
style="display:inline;fill:#985289;fill-opacity:1;stroke-width:0.71608"
d="m -1217.7495,-1051.685 c 0.2256,-0.4349 0.3684,-1.4225 0.4858,-3.3629 0.091,-1.5109 0.2939,-3.6851 0.45,-4.8312 1.4151,-10.3986 1.5639,-26.4284 0.3735,-40.2652 -0.049,-0.5708 -0.1398,-1.8477 -0.2013,-2.8377 -0.061,-0.9899 -0.1502,-2.2262 -0.1968,-2.7472 -0.046,-0.521 -0.2147,-2.6951 -0.3734,-4.8314 -0.1587,-2.1361 -0.4987,-5.3759 -0.7555,-7.1995 -0.2568,-1.8236 -0.7225,-5.3619 -1.0348,-7.8628 -0.3124,-2.5009 -0.742,-5.5275 -0.9548,-6.7259 -0.5178,-2.9164 -1.277,-7.2826 -1.6306,-9.3783 -0.3249,-1.925 -0.8521,-4.5335 -1.3976,-6.9154 -0.589,-2.5717 -0.9336,-4.229 -1.2171,-5.8531 -0.1455,-0.8332 -0.4947,-2.0905 -0.7761,-2.7938 -1.7507,-4.3761 2.0554,-5.1067 9.0031,-3.9969 1.7764,0.2837 3.1253,0.6956 4.6444,1.0865 9.4816,2.7152 19.2492,5.0043 28.4168,8.7035 8.6259,3.4361 17.1419,7.2847 25.388,11.7429 2.375,1.2983 7.1398,4.4313 10.1363,6.6647 3.2272,2.4056 4.9495,2.6829 3.1139,0.5013 -0.2538,-0.3015 -3.3239,-3.2854 -4.8265,-4.4979 -0.4545,-0.3648 -1.0864,-0.8828 -1.4042,-1.1513 -0.3177,-0.2684 -1.459,-1.0784 -2.5361,-1.7999 -2.2452,-1.5038 -3.6749,-2.4889 -5.2628,-3.6262 -3.0312,-2.1712 -3.9258,-2.779 -5.846,-3.9724 -0.3572,-0.2221 -3.1891,-1.9679 -5.0476,-2.9577 -1.4671,-0.7814 -9.4465,-5.5474 -12.1993,-6.9444 -4.6782,-2.4616 -17.3614,-8.7704 -33.4401,-14.1486 -3.445,-1.1644 -5.9042,-1.8939 -9.189,-2.726 -2.0842,-0.5279 -4.2721,-1.0958 -4.8623,-1.2616 -6.4763,-1.8215 -10.378,2.9089 -7.7478,9.3934 0.1941,0.4785 0.4596,1.2963 0.5902,1.8173 0.1304,0.521 0.2999,1.0326 0.3764,1.1367 0.077,0.1043 0.4952,1.1274 0.9303,2.2736 0.4351,1.1463 0.9596,2.4417 1.1655,2.8786 0.206,0.4369 0.4883,1.2043 0.627,1.7052 0.3444,1.2421 1.3941,4.0412 1.6157,4.3081 0.099,0.1195 0.2614,0.5549 0.3605,0.9677 0.099,0.4127 0.4525,1.4751 0.7851,2.3607 0.3327,0.8859 0.7196,2.0368 0.8599,2.5579 0.4365,1.6219 1.1991,4.8734 1.4219,6.0627 0.1172,0.6252 0.3251,1.6483 0.4621,2.2736 0.1373,0.6252 0.3428,1.6909 0.4569,2.3683 0.4827,2.8633 1.1917,6.5772 1.3563,7.1048 0.1796,0.5756 0.5879,2.3891 1.4287,6.3471 0.8019,3.7744 2.0725,11.3353 2.6586,15.82 0.404,3.0914 0.3674,2.7931 0.534,4.3578 0.078,0.7294 0.2574,2.1361 0.3993,3.126 0.3641,2.5401 0.6153,4.679 0.8455,7.1996 0.2403,2.6305 0.3003,3.2443 0.6853,7.0102 0.4279,4.1849 0.5436,15.1848 0.2402,22.8303 -0.3113,7.8433 -0.2926,9.6345 0.1069,10.2443 0.4011,0.6121 0.5997,0.5808 0.9816,-0.1555 z"
id="path21284" /><path
style="display:inline;fill:#985289;fill-opacity:1;stroke-width:0.71608"
d="m -1405.616,-1053.4513 c 0.312,-0.376 0.2615,-1.4618 -0.1972,-4.2491 -1.7144,-10.4147 -0.6072,-29.6521 2.857,-49.6393 0.2257,-1.3025 0.5179,-3.1356 0.6494,-4.0733 0.8044,-5.7413 1.7895,-10.9696 2.8738,-15.2519 0.8421,-3.3257 1.0755,-4.319 1.7589,-7.4837 0.2025,-0.9379 0.6736,-2.984 1.0471,-4.5472 0.3733,-1.563 0.8543,-3.6945 1.0686,-4.7365 0.2316,-1.1251 0.657,-2.4333 1.0475,-3.2209 0.6361,-1.2826 1.3706,-3.3267 2.0427,-5.6839 0.1781,-0.6252 0.8395,-2.1598 1.4697,-3.4103 1.0647,-2.1129 1.478,-3.1615 3.6588,-9.2836 0.4454,-1.2505 0.9676,-2.5721 1.1605,-2.9367 0.9121,-1.7253 -0.01,-5.9466 -1.6528,-7.5927 -0.9669,-0.9668 -3.1907,-1.1914 -5.3857,-0.5439 -0.3126,0.092 -0.8668,0.2014 -1.2315,0.2427 -0.3647,0.042 -0.791,0.12 -0.9473,0.1747 -0.1563,0.054 -1.0515,0.276 -1.9894,0.4917 -1.807,0.4156 -2.1263,0.4833 -4.0735,0.8652 -10.4605,2.5271 -20.2397,7.0816 -30.2154,11.0148 -3.2145,1.0811 -6.1141,2.673 -9.0981,4.2484 -0.8336,0.4398 -1.7288,0.9361 -1.9894,1.1028 -0.2605,0.1668 -0.8146,0.4857 -1.2315,0.7087 -0.4168,0.2231 -1.1415,0.6227 -1.6104,0.8881 -0.4689,0.2656 -1.492,0.8452 -2.2735,1.2879 -0.7815,0.4429 -1.8047,1.0081 -2.2736,1.2559 -0.469,0.2478 -1.1935,0.6628 -1.6104,0.9222 -0.4169,0.2594 -1.0573,0.6329 -1.4231,0.8301 -0.3659,0.1974 -1.374,0.7936 -2.2401,1.3252 -0.8662,0.5317 -1.6123,0.9665 -1.6579,0.9665 -0.1062,0 -2.1987,1.3464 -5.0994,3.281 -2.1815,1.455 -3.5593,2.5103 -6.4417,4.9337 -0.7295,0.6132 -2.0243,1.5682 -2.8774,2.1221 -0.8531,0.5539 -1.7269,1.1616 -1.942,1.3506 -4.433,3.8951 -2.7137,4.7633 2.1123,1.0667 2.6573,-2.0354 12.8868,-7.643 17.201,-9.4292 0.469,-0.1942 1.2788,-0.5458 1.8,-0.7815 0.3047,-0.138 8.464,-3.4405 9.7572,-4.0201 4.0687,-1.7679 16.6441,-5.573 22.1672,-7.0412 1.6151,-0.4295 3.4056,-0.9169 3.9787,-1.0834 1.5444,-0.4486 4.7972,-1.1271 8.4311,-1.7585 1.7715,-0.3078 3.9029,-0.6917 4.7367,-0.8532 5.3344,-1.0334 6.3323,0.7796 4.2707,7.7603 -0.092,0.3126 -0.3121,1.2078 -0.4884,1.9893 -0.1763,0.7815 -0.6866,2.7426 -1.1338,4.3576 -0.9973,3.6025 -1.1421,4.217 -1.6965,7.1997 -0.2422,1.3025 -0.9077,4.6702 -1.479,7.4838 -0.5715,2.8135 -1.2244,6.2239 -1.451,7.5784 -0.2267,1.3548 -0.535,3.1878 -0.6855,4.0736 -0.1503,0.8856 -0.3532,2.2072 -0.4508,2.9366 -0.098,0.7295 -0.3958,2.7486 -0.6626,4.487 -0.2666,1.7384 -0.6078,4.4667 -0.7582,6.0628 -0.1505,1.5961 -0.4463,4.5219 -0.6574,6.5018 -0.9168,8.5943 -1.3076,26.2731 -0.6983,31.5831 0.063,0.5525 0.2378,2.8376 0.3875,5.078 0.1631,2.442 0.3949,4.4783 0.5788,5.0844 0.1688,0.556 0.3763,1.8777 0.461,2.937 0.1454,1.8158 0.267,2.2176 1.0851,3.5833 0.1483,0.2476 0.7646,0.1535 1.0215,-0.156 z"
id="path21282" /><path
style="display:inline;fill:#985289;fill-opacity:1;stroke-width:0.71608"
d="m -1548.2398,-987.38482 c 0.4745,-0.50736 1.1295,-1.38126 1.4553,-1.94199 0.3259,-0.56074 0.6691,-1.14741 0.7625,-1.30371 0.093,-0.1563 0.324,-0.66636 0.5125,-1.13346 1.4925,-3.69977 4.3377,-8.80252 7.7073,-13.82282 0.7949,-1.1842 1.4453,-2.1794 1.4453,-2.2115 0,-0.032 1.1724,-1.6307 2.6051,-3.5523 1.4328,-1.9217 2.733,-3.6844 2.8894,-3.9172 0.3416,-0.5087 4.2475,-5.3849 4.8508,-6.0557 0.2341,-0.2605 0.8366,-0.9426 1.3386,-1.5157 0.502,-0.5731 1.5163,-1.6815 2.2538,-2.463 0.7375,-0.7816 2.195,-2.4015 3.239,-3.5998 1.0438,-1.1984 2.6577,-2.9661 3.5863,-3.9283 5.9289,-6.1435 8.8047,-9.0873 9.5511,-9.7771 0.4689,-0.4334 2.2603,-2.1523 3.9808,-3.8196 1.7205,-1.6675 3.6509,-3.5028 4.2898,-4.0786 0.6389,-0.5759 1.5449,-1.5176 2.0135,-2.0928 0.4685,-0.5753 1.3515,-1.423 1.9622,-1.884 0.6106,-0.4611 1.6219,-1.3415 2.2473,-1.9564 0.6254,-0.6149 1.8772,-1.7832 2.7818,-2.5962 0.9046,-0.813 2.226,-2.0264 2.9367,-2.6963 0.7105,-0.6701 2.2298,-2.0191 3.376,-2.9977 1.1462,-0.9786 2.4007,-2.0678 2.7878,-2.4202 0.3869,-0.3525 0.952,-0.8219 1.2557,-1.0431 0.3036,-0.2211 1.4477,-1.1269 2.5425,-2.0126 1.0948,-0.8858 2.4054,-1.9262 2.9125,-2.312 0.507,-0.3859 1.3908,-1.0671 1.964,-1.5141 10.6223,-8.2831 14.2924,-10.0768 17.0767,-8.3453 1.0385,0.6458 1.6801,3.2202 1.6801,6.7414 0,0.8713 0.077,1.8313 0.17,2.1335 0.093,0.3021 0.2756,1.1888 0.4049,1.9703 0.4971,3.0072 1.2964,6.6138 1.8688,8.431 0.5662,1.7981 1.1631,3.9151 1.8163,6.4418 0.1616,0.6252 0.3776,1.2848 0.48,1.4657 0.1024,0.181 0.186,0.5017 0.186,0.7126 0,0.211 0.1635,0.745 0.3631,1.1867 0.1997,0.4418 0.5522,1.3573 0.7832,2.0346 0.2311,0.6774 0.5718,1.5726 0.7568,1.9894 0.1853,0.4168 0.3875,1.0563 0.4494,1.4209 0.062,0.3648 0.2474,0.919 0.4119,1.2316 0.1647,0.3126 0.6363,1.4515 1.0482,2.5309 0.4118,1.0793 1.2204,2.9075 1.7966,4.0624 0.5763,1.155 1.2567,2.5962 1.5119,3.2027 0.8664,2.0582 5.8144,10.7252 6.3599,11.1399 0.069,0.052 0.6232,0.8781 1.2322,1.8353 1.0459,1.6441 3.2449,4.9699 4.1106,6.2166 1.3619,1.9615 3.5329,5.2228 3.5329,5.3074 0,0.055 0.4095,0.6315 0.9099,1.2805 0.5005,0.649 1.0434,1.3933 1.2064,1.6538 0.163,0.2605 0.8682,1.1557 1.567,1.9893 0.6988,0.8336 1.4347,1.8317 1.6353,2.2181 0.416,0.8012 1.7336,1.6659 2.5384,1.6659 0.705,0 0.5911,-1.1844 -0.2141,-2.2259 -0.6366,-0.8234 -0.814,-1.0771 -2.0387,-2.9129 -0.5129,-0.7687 -1.0817,-1.5682 -1.2642,-1.7766 -0.6259,-0.7149 -3.4461,-5.3512 -4.7252,-7.768 -0.5516,-1.042 -1.2021,-2.1915 -1.4456,-2.5543 -0.2435,-0.3629 -0.4428,-0.753 -0.4428,-0.8671 0,-0.1141 -0.3624,-0.8829 -0.8053,-1.7085 -1.4465,-2.6963 -2.6186,-5.2026 -3.1656,-6.769 -0.5311,-1.5206 -2.0645,-5.4653 -2.3713,-6.1001 -0.1512,-0.3126 -0.4399,-0.9946 -0.6418,-1.5157 -0.2017,-0.521 -0.6368,-1.571 -0.9668,-2.3334 -0.3299,-0.7623 -0.9714,-2.4249 -1.4253,-3.6945 -1.3543,-3.7874 -1.7917,-4.9356 -2.0703,-5.4345 -0.2673,-0.4786 -0.6724,-1.8022 -1.1886,-3.8841 -0.155,-0.6251 -0.4853,-1.8188 -0.734,-2.6524 -0.2487,-0.8337 -0.7134,-2.624 -1.0328,-3.9788 -0.3193,-1.3545 -0.6723,-2.804 -0.7843,-3.2208 -0.2455,-0.9138 -0.2426,-0.8952 -0.6278,-3.9787 -0.1692,-1.3547 -0.3864,-2.7614 -0.4826,-3.1262 -0.096,-0.3647 -0.2194,-1.1319 -0.2734,-1.7052 -0.1823,-1.9329 -0.6274,-8.2332 -0.6559,-9.2836 -0.015,-0.5732 -0.1149,-2.193 -0.221,-3.5998 -0.106,-1.4068 -0.1805,-3.5808 -0.1655,-4.8312 0.037,-3.0537 -0.1931,-7.1112 -0.4291,-7.5787 -0.3257,-0.6454 -1.7529,-1.8512 -2.0699,-1.9893 -0.4611,-0.201 -0.7996,-0.6909 -2.9084,-0.59 -1.7376,0.083 -6.6203,2.0008 -9.5065,3.7338 -2.0191,1.2123 -3.3798,1.9813 -3.5452,2.1164 -0.8281,0.6766 -1.6979,1.2016 -2.8117,1.9904 -1.0874,0.7703 -1.4924,1.1562 -2.1879,1.7326 -0.5965,0.5643 -1.7239,1.5209 -2.5054,2.1258 -0.7816,0.6048 -1.5179,1.1877 -1.6363,1.2951 -0.8827,0.8002 -2.4055,2.0442 -3.546,2.8971 -0.7449,0.5569 -1.8232,1.4604 -2.3963,2.0076 -0.573,0.5472 -1.3404,1.1971 -1.7051,1.4442 -0.3647,0.2471 -1.1431,0.8819 -1.7297,1.4107 -0.5867,0.5288 -1.7375,1.5559 -2.5578,2.2823 -0.8201,0.7265 -2.0879,1.9457 -2.8174,2.7094 -2.6641,2.7895 -6.3887,6.4937 -7.1858,7.1469 -0.9144,0.7492 -6.9603,6.815 -9.2974,9.3279 -0.8337,0.8963 -2.5815,2.723 -3.884,4.0595 -2.9889,3.0664 -4.507,4.7225 -5.4943,5.9935 -0.4281,0.5511 -1.1201,1.3184 -1.538,1.7052 -0.4178,0.3869 -0.8763,0.9164 -1.0191,1.1769 -0.1426,0.2606 -0.9917,1.3263 -1.8869,2.3683 -4.7636,5.5451 -6.7131,8.0627 -9.6831,12.5046 -0.9059,1.3546 -1.9344,2.858 -2.2858,3.341 -0.3514,0.4829 -0.6394,0.9518 -0.6399,1.042 -4e-4,0.09 -0.4907,0.8147 -1.0893,1.6099 -0.5988,0.7951 -1.6427,2.3954 -2.3201,3.556 -0.6773,1.1606 -1.9354,3.3078 -2.7955,4.7715 -1.3339,2.2696 -6.0445,11.5795 -7.7089,15.2358 -0.2847,0.6252 -0.7597,1.6483 -1.0557,2.27353 -3.1557,6.66548 -4.7319,10.2463 -4.7328,10.75201 0,0.80537 0.5779,0.65706 1.5681,-0.40146 z"
id="path21280" /><path
style="display:inline;fill:#985289;fill-opacity:1;stroke-width:0.71608"
d="m -1073.8029,-987.09522 c -0.2085,-1.27219 -0.482,-2.2738 -0.8278,-3.03123 -0.198,-0.43355 -0.4916,-1.08666 -0.6523,-1.45138 -0.1609,-0.36472 -0.3727,-0.79101 -0.4707,-0.94731 -0.098,-0.15631 -0.5328,-1.05153 -0.9664,-1.98937 -1.1716,-2.45971 -2.0579,-4.97585 -3.5356,-7.29429 -0.121,-0.1564 -0.6584,-1.1795 -1.1942,-2.2736 -1.1727,-2.3951 -1.3912,-2.8022 -2.4028,-4.4774 -0.4321,-0.7157 -1.3597,-2.4096 -2.0612,-3.7643 -0.7014,-1.3546 -1.3377,-2.5482 -1.4138,-2.6524 -0.076,-0.1042 -0.4842,-0.8289 -0.9067,-1.6105 -0.4225,-0.7815 -1.0683,-1.9325 -1.4351,-2.5577 -0.3669,-0.6252 -0.8386,-1.4997 -1.0482,-1.9432 -0.5408,-1.1442 -5.7659,-9.0685 -7.3667,-11.1723 -0.4936,-0.6486 -1.5795,-2.1298 -2.4131,-3.2915 -0.8336,-1.1617 -1.7159,-2.3361 -1.9607,-2.6099 -0.2448,-0.2737 -0.8203,-1.0669 -1.2789,-1.7624 -0.4587,-0.6956 -1.7097,-2.2303 -2.78,-3.4104 -1.8795,-2.0723 -3.5959,-4.0701 -4.7798,-5.5637 -2.1676,-2.7347 -14.2706,-15.5588 -17.1359,-18.1572 -0.359,-0.3255 -2.2748,-2.2119 -4.2573,-4.1917 -1.9826,-1.9799 -4.1745,-4.0687 -4.8711,-4.6418 -0.6965,-0.5732 -2.5621,-2.1321 -4.1457,-3.4643 -2.4687,-2.0769 -11.4131,-8.9561 -14.171,-10.8988 -0.4895,-0.3449 -1.2419,-0.9418 -1.6717,-1.3263 -0.4299,-0.3845 -1.2825,-0.9929 -1.8947,-1.3519 -2.7876,-2.0959 -5.5744,-3.6163 -8.756,-4.8618 -3.8438,-1.5265 -6.4561,-1.3706 -8.3341,0.4975 -1.1404,1.1343 -1.1133,0.9971 -1.2673,6.438 -0.1196,4.2279 -0.3708,8.1391 -0.6606,10.2852 -0.1014,0.7518 -0.2222,1.6652 -0.2684,2.0299 -0.046,0.3647 -0.2574,1.8567 -0.4694,3.3156 -0.2119,1.4589 -0.4782,3.4197 -0.5917,4.3576 -0.1136,0.9379 -0.3264,2.2168 -0.473,2.842 -0.1467,0.6252 -0.531,2.5435 -0.8542,4.2629 -0.9956,5.2981 -2.8717,11.6423 -5.4067,18.2831 -0.2784,0.7295 -0.7381,1.9657 -1.0214,2.7473 -0.5141,1.4179 -2.1056,5.4595 -2.5739,6.5364 -0.136,0.3127 -0.5414,1.2505 -0.9011,2.0842 -0.3595,0.8336 -0.8263,1.8993 -1.0372,2.3682 -0.2109,0.469 -0.9542,2.1314 -1.6518,3.6946 -1.141,2.5568 -2.3999,5.0735 -5.1179,10.231 -0.9403,1.7843 -1.3125,2.417 -2.7943,4.7515 -0.495,0.7797 -0.9,1.4581 -0.9,1.5074 0,0.049 -0.4476,0.6788 -0.9946,1.3988 -1.1682,1.5376 -1.6478,2.1887 -2.0369,2.7652 -0.1561,0.2316 -0.5136,0.7521 -0.7941,1.1566 -0.7201,1.0387 -0.7861,1.8723 -0.1483,1.8723 0.464,0 3.0386,-2.4983 4.0687,-3.9482 0.2084,-0.2933 0.5494,-0.7289 0.7578,-0.9679 0.784,-0.8992 1.0344,-1.2438 1.3651,-1.8798 0.187,-0.3597 0.6955,-1.0665 1.1299,-1.5709 0.4344,-0.5043 1.0739,-1.3128 1.4209,-1.7968 0.3472,-0.4839 1.2019,-1.6349 1.8995,-2.5577 1.25,-1.6537 2.5486,-3.4145 3.613,-4.8987 0.5239,-0.7308 2.1255,-3.1219 3.5922,-5.3635 0.6158,-0.9411 3.298,-5.3098 3.7447,-6.0991 0.088,-0.1562 0.7721,-1.5204 1.5194,-3.0313 0.7472,-1.511 1.4202,-2.8325 1.4955,-2.9368 0.075,-0.1041 0.3796,-0.8288 0.6764,-1.6103 0.47,-1.2376 1.2426,-3.0953 2.0881,-5.0208 0.6611,-1.5059 0.9385,-2.2065 1.4623,-3.6946 1.056,-2.999 1.4437,-4.07 1.9545,-5.3996 0.7763,-2.0199 1.8005,-5.35 2.0688,-6.726 0.132,-0.6773 0.3756,-1.743 0.5413,-2.3683 0.9102,-3.4354 1.4072,-6.5059 1.5487,-9.5678 0.2417,-5.2331 0.8061,-7.1291 2.3278,-7.8203 2.1755,-0.9882 5.1064,-0.1505 8.7253,2.4935 3.0894,2.2574 4.5547,3.2503 5.3397,3.6187 0.469,0.22 1.1937,0.7328 1.6105,1.1398 0.4168,0.4069 1.0754,0.8719 1.4634,1.0335 0.8381,0.3488 4.774,3.6266 5.6935,4.257 0.4031,0.2763 0.62,0.4798 0.7922,0.5827 0.1288,0.077 0.7013,0.4507 1.3721,0.9034 0.6046,0.4969 1.3124,0.9973 1.5729,1.1119 0.2605,0.1144 0.9,0.5835 1.421,1.0423 0.521,0.4588 1.2883,1.0644 1.7052,1.3459 0.6838,0.4617 2.4522,1.9606 5.968,5.0582 0.6773,0.5968 1.9562,1.7076 2.8419,2.4685 0.8858,0.7611 3.0738,2.8429 4.8624,4.6262 1.7885,1.7834 5.6219,5.5871 8.5187,8.4528 7.1129,7.0364 11.0669,11.2398 13.9759,14.857 0.5098,0.6339 1.4476,1.7113 2.0841,2.3943 1.712,1.8371 2.2126,2.4194 3.2492,3.779 0.5163,0.6773 1.4931,1.8413 2.1706,2.5866 1.7833,2.585 4.0616,4.8036 5.7789,7.4295 0.1043,0.1633 0.5732,0.8812 1.0421,1.5954 0.4689,0.7144 0.9378,1.446 1.0421,1.6261 0.1041,0.1802 0.4546,0.6644 0.7788,1.0762 1.696,2.9736 3.6613,5.7482 5.3849,8.70596 0.316,0.52102 0.7207,1.24571 0.8993,1.61044 0.3263,0.66622 1.3013,1.94784 3.5274,6.347 2.1518,4.25234 3.9333,6.42391 3.5996,4.38788 z"
id="path21278" /><path
style="display:inline;fill:#812973;fill-opacity:1;stroke-width:0.71608"
d="m -1257.546,-1026.1934 c 0.6031,-0.6361 0.9273,-1.0595 1.8353,-2.3965 0.8999,-1.3251 4.7147,-8.2521 5.1155,-9.2887 0.141,-0.3646 0.319,-0.7483 0.3956,-0.8526 0.077,-0.1041 0.4471,-0.9993 0.8234,-1.9893 0.3763,-0.9899 0.8463,-2.2262 1.0446,-2.7472 4.7917,-12.5948 6.1783,-29.6549 3.357,-41.3029 -0.9629,-3.9756 -1.1208,-4.5922 -1.5762,-6.1575 -0.2576,-0.8857 -0.855,-2.8041 -1.3274,-4.2629 -0.4724,-1.4588 -0.9704,-3.0361 -1.1066,-3.5051 -2.0915,-7.2017 -5.9507,-14.1466 -11.3704,-20.462 -9.2906,-10.8261 -19.217,-18.4629 -29.3973,-22.6161 -4.136,-1.974 -8.4005,-3.4284 -12.8558,-4.5535 -3.473,-0.8336 -13.2154,-0.8312 -16.5691,0 -0.5731,0.1428 -1.4257,0.3521 -1.8946,0.4652 -1.9029,0.4591 -5.2317,1.3855 -5.5516,1.5451 -0.1878,0.093 -0.9746,0.336 -1.7487,0.5388 -0.774,0.2026 -1.7714,0.5684 -2.2164,0.8126 -0.445,0.2443 -1.4209,0.675 -2.1688,0.9571 -0.7479,0.2821 -1.4148,0.7082 -1.5985,0.8005 -1.0377,0.5218 -2.6866,1.1875 -3.5783,1.7035 -0.4689,0.2714 -1.5772,0.8799 -2.4629,1.3523 -0.8858,0.4725 -1.7384,0.9639 -1.8947,1.092 -0.1563,0.1282 -1.3926,0.9409 -2.7471,1.8059 -2.5197,1.6087 -4.4044,3.046 -6.9091,5.2683 -0.7789,0.6911 -1.845,1.6335 -2.3691,2.0942 -1.4987,1.3175 -5.6598,5.6044 -7.4854,7.7117 -3.4825,4.0196 -6.8733,9.1388 -8.9651,13.5349 -3.5094,7.3748 -5.0589,11.7482 -7.2125,20.3554 -1.2036,4.8107 -1.6275,9.3482 -1.6285,17.4305 -7e-4,5.3576 0.05,6.1544 0.7294,11.4625 0.623,4.868 2.3114,11.5479 3.7114,14.6835 0.2327,0.521 0.5262,1.2883 0.6522,1.7052 0.3681,1.2176 0.7938,2.1783 1.6374,3.6944 0.4348,0.7816 0.9873,1.9094 1.2277,2.5063 0.2404,0.5969 1.0001,1.7904 1.688,2.6524 1.562,1.9569 2.3013,3.3245 2.9505,4.1498 1.6195,2.0588 1.8123,2.3259 4.1569,-0.085 1.0457,-1.0751 2.7112,-2.5944 3.7012,-3.3763 0.9899,-0.7819 1.9276,-1.544 2.084,-1.6934 2.1254,-2.032 12.4121,-7.9433 13.8227,-7.9433 0.1169,0 0.529,-0.1598 0.9156,-0.3553 0.3866,-0.1955 1.8539,-0.7509 3.2607,-1.2344 1.4067,-0.4836 3.1546,-1.0963 3.884,-1.3618 0.7294,-0.2655 1.8378,-0.6413 2.463,-0.8352 0.6253,-0.1939 1.5205,-0.4799 1.9893,-0.6356 1.3066,-0.4338 4.825,-1.2345 6.726,-1.5307 0.9379,-0.1461 2.0035,-0.3593 2.3683,-0.4738 2.3607,-0.741 15.5243,-0.8001 19.7041,-0.088 5.1718,0.8807 6.0151,1.065 11.1783,2.4441 1.9253,0.5143 2.6047,0.7456 4.7366,1.6127 1.0942,0.4449 2.5009,1.0162 3.1262,1.2694 1.8824,0.762 7.4729,3.6337 8.7127,4.4754 0.6378,0.4329 2.3872,1.5675 3.1188,2.1031 1.7928,1.3128 3.0385,2.1835 4.5487,3.3861 0.5486,0.4371 1.3362,1.1459 1.8064,1.5817 0.4702,0.4357 1.0741,0.962 1.9411,1.6489 1.6058,1.272 2.1852,1.7286 2.7442,2.1014 1.2472,0.8317 1.6522,1.6687 2.4773,0.7985 z"
id="path21276-14-5" /><path
style="display:inline;fill:#6d2361;fill-opacity:1;stroke-width:0.71608"
d="m -1266.7895,-849.43329 c 0.5355,-0.39077 1.6797,-1.21739 2.5426,-1.83693 0.8631,-0.61953 2.1659,-1.60301 2.8954,-2.18549 0.7294,-0.58249 1.5448,-1.19686 1.8121,-1.36527 1.6596,-1.04601 4.7513,-4.36249 7.4678,-8.01086 0.4807,-0.64546 0.9728,-1.25106 3.0844,-3.79521 0.1729,-0.20841 0.6589,-0.89545 1.0799,-1.52675 5.1167,-7.67235 4.8088,-9.09303 -4.3032,-19.86164 -1.2246,-1.44735 -2.7426,-3.35301 -3.3734,-4.2348 -0.6308,-0.8818 -1.2281,-1.70117 -1.3273,-1.82084 -1.2908,-1.55591 -8.5973,-11.73258 -10.0137,-13.94728 -1.5488,-2.42196 -1.2465,-2.51023 -9.5836,2.79801 -0.8337,0.53077 -2.0995,1.32539 -2.8133,1.76581 -1.5579,0.96155 -1.6837,2.29848 -0.3186,3.38729 0.065,0.0521 0.3236,0.39313 0.5741,0.75785 0.2505,0.36472 0.5029,0.70575 0.561,0.75785 0.058,0.0521 0.4422,0.64891 0.8537,1.32624 0.4114,0.67733 1.0997,1.70043 1.5296,2.27355 0.4299,0.57312 0.839,1.16993 0.9093,1.32624 0.071,0.15631 0.5102,0.75311 0.9775,1.32624 1.6455,2.01797 3.4702,4.47538 4.9239,6.6312 0.8081,1.19835 1.5272,2.22145 1.5982,2.27355 0.1737,0.12754 2.6671,3.96245 2.6671,4.10199 0,0.0606 0.3197,0.54508 0.7105,1.07665 0.3907,0.53156 0.8064,1.13979 0.9235,1.35162 0.1173,0.21183 0.6434,1.09888 1.1693,1.97123 1.6317,2.70629 1.8455,5.95544 1.019,7.79233 -1.4201,3.15641 -2.539,6.52626 -3.7065,9.82355 -0.2724,0.76585 -1.0478,2.48736 -1.7231,3.82559 -2.4441,4.84341 -2.474,5.72407 -0.1362,4.01828 z"
id="path21271" /><path
style="display:inline;fill:#6d2361;fill-opacity:1;stroke-width:0.71608"
d="m -1360.5688,-848.69619 c 0.035,-0.2338 -0.1948,-0.80398 -0.521,-1.2965 -0.3203,-0.48356 -0.504,-1.04067 -0.5824,-1.19682 -1.0557,-2.10404 -2.0403,-5.59536 -3.2309,-9.2115 -0.8697,-3.39263 -1.0995,-4.44692 -1.5056,-8.25149 -0.2906,-2.72343 0.7082,-4.66206 2.8473,-7.73189 1.1187,-1.60539 2.5854,-3.78534 4.8441,-7.1996 0.6549,-0.98995 1.4089,-2.04762 1.6758,-2.35039 0.2666,-0.30277 0.4872,-0.60117 0.49,-0.66312 0,-0.062 0.3516,-0.49091 0.7751,-0.95328 0.4237,-0.46234 0.9969,-1.14274 1.2739,-1.51196 0.277,-0.36923 0.6446,-0.84351 0.8168,-1.05398 0.1723,-0.21047 0.8531,-1.06473 1.513,-1.89837 0.6599,-0.83364 1.2862,-1.61877 1.3919,-1.74474 0.1058,-0.12596 0.4906,-0.62397 0.8555,-1.10666 0.3646,-0.48269 1.7927,-2.22918 3.1733,-3.88106 3.0229,-3.61653 3.0584,-3.95483 0.516,-4.92144 -0.2531,-0.0962 -1.3293,-0.75331 -2.3915,-1.46014 -1.0621,-0.70681 -2.8181,-1.85314 -3.9022,-2.54738 -1.084,-0.69424 -2.1138,-1.38298 -2.2884,-1.53052 -1.2114,-1.02354 -2.1925,-1.28463 -2.5421,-0.67648 -0.9294,1.61649 -3.2702,4.8717 -6.2417,8.67948 -0.4473,0.57313 -1.6135,2.10778 -2.5916,3.41032 -0.9781,1.30256 -1.984,2.62407 -2.2351,2.93667 -0.2511,0.31263 -0.6285,0.78154 -0.8386,1.04206 -0.572,0.70951 -2.0223,2.38107 -3.2786,3.77877 -0.6141,0.6831 -1.8495,2.1372 -2.7455,3.23135 -0.8959,1.09415 -1.9,2.28776 -2.2311,2.65248 -0.3312,0.36472 -0.798,0.9189 -1.0374,1.23151 -0.2393,0.31261 -0.4863,0.61103 -0.5488,0.66313 -2.75,2.29064 -3.6562,7.31753 -2.0075,11.13601 0.8388,1.94262 2.4099,4.66651 2.975,5.15779 0.06,0.0521 0.4803,0.64892 0.9342,1.32626 2.5655,3.82826 5.1557,6.9409 7.4826,8.99167 0.9379,0.82653 2.1072,1.88607 2.5986,2.35453 0.4913,0.46846 1.0455,0.93331 1.2315,1.03299 0.2831,0.15176 2.2094,1.69445 2.5169,2.01576 1.765,1.84365 2.6901,2.35308 2.8085,1.54654 z"
id="path21270" /><path
style="display:inline;fill:#812973;fill-opacity:1;stroke-width:0.71608"
d="m -1542.7376,-871.23997 c 0.2862,-0.71515 0.3795,-1.94635 0.6059,-7.98636 0.3653,-9.74366 2.0798,-19.54708 4.4068,-25.19856 0.4076,-0.98996 1.1027,-2.73252 1.5447,-3.87237 1.2218,-3.15114 4.0398,-8.5793 6.3448,-12.2215 4.396,-6.94599 8.186,-12.56318 11.3827,-16.86998 1.3546,-1.82507 2.6392,-3.57623 2.8546,-3.89147 0.2155,-0.31523 0.5298,-0.74153 0.6984,-0.94732 0.1686,-0.20577 0.8479,-1.09883 1.5094,-1.98457 1.3806,-1.84865 3.2151,-4.21831 3.3734,-4.35765 0.059,-0.0521 0.5291,-0.64891 1.0442,-1.32625 0.515,-0.67732 1.5341,-1.90806 2.2646,-2.73496 0.7306,-0.82689 1.9252,-2.19642 2.6546,-3.04337 0.7295,-0.84697 1.7526,-1.99705 2.2736,-2.55575 0.521,-0.5587 1.4702,-1.57641 2.1094,-2.26158 0.6392,-0.68517 1.4791,-1.62942 1.8666,-2.09834 0.3874,-0.46893 1.4509,-1.66254 2.3633,-2.65249 0.9122,-0.98994 2.0126,-2.21084 2.4452,-2.7131 0.4325,-0.50226 1.8728,-1.97506 3.2005,-3.27287 3.4847,-3.40602 8.8565,-8.74429 10.2086,-10.14491 1.7687,-1.83199 3.8756,-3.82394 4.6153,-4.36366 0.3648,-0.26605 0.9616,-0.76408 1.3263,-1.10671 0.3648,-0.34263 1.132,-0.94417 1.7052,-1.33677 0.5731,-0.3926 1.2422,-0.9045 1.5455,-1.11477 0.282,-0.19555 0.6671,-0.43095 0.8166,-0.53453 0.135,-0.0936 0.4514,-0.21475 0.764,-0.42178 0.3127,-0.20703 0.4612,-0.17118 0.9836,-0.37768 2.2253,-0.87975 4.476,0.11177 6.1894,1.57586 0.3272,0.40939 1.064,1.27474 1.637,1.92299 0.5732,0.64828 1.4425,1.68744 1.9319,2.30928 1.2944,1.64486 2.1747,2.5513 3.5625,3.66878 0.6774,0.54536 1.6578,1.44392 2.179,1.99682 0.5767,0.6121 1.7993,1.51779 3.126,2.31576 1.1984,0.72078 2.5199,1.65154 2.9367,2.06837 0.4168,0.41683 1.2268,1.01548 1.7999,1.33036 1.8397,1.01067 7.0653,3.59861 8.3363,4.12849 4.1139,2.01877 8.5144,3.56127 12.9783,4.62318 0.521,0.0928 1.5868,0.31849 2.3683,0.50167 2.9593,0.69362 3.9531,-0.25782 1.5541,-1.48799 -0.2817,-0.14444 -1.0732,-0.56358 -1.759,-0.93143 -0.6857,-0.36784 -2.0072,-0.98636 -2.9366,-1.37447 -6.1224,-2.55672 -14.0957,-7.14843 -16.6574,-9.59277 -0.521,-0.49715 -1.2457,-1.14719 -1.6105,-1.44454 -2.1172,-1.7262 -5.7038,-5.35554 -8.5522,-8.65448 -0.3502,-0.40558 -1.2722,-1.41431 -2.0488,-2.24163 -1.7108,-1.82231 -2.0356,-2.65361 -2.2824,-3.01532 -1.2058,-1.76765 -1.7477,-2.63188 -2.5598,-4.92227 -1.559,-3.69329 -2.2226,-4.54719 -3.5978,-5.47479 -0.4462,-0.301 -1.1291,-0.257 -1.7301,-0.3396 -1.1835,-0.03 -2.6585,-0.083 -4.3961,0.6424 -4.2764,2.2443 -4.4754,2.3756 -7.0415,4.66753 -0.6589,0.58851 -1.421,1.51352 -1.8841,1.86936 -0.4632,0.35582 -1.1874,1.03063 -1.6094,1.49955 -0.422,0.46892 -2.171,2.21671 -3.8866,3.88398 -5.6083,5.44995 -8.9789,8.87071 -10.7135,10.8728 -0.9379,1.08242 -2.3446,2.62265 -3.1262,3.42272 -0.7815,0.80008 -2.1027,2.21644 -2.9358,3.14746 -0.8331,0.93103 -1.9415,2.12332 -2.463,2.64954 -0.5215,0.52622 -1.4145,1.50668 -1.9842,2.17882 -0.5699,0.67213 -1.4117,1.5631 -1.8708,1.97992 -1.9676,1.7866 -2.8699,2.74685 -4.0058,4.26292 -0.6637,0.88573 -1.3537,1.78095 -1.5334,1.98935 -0.1797,0.20841 -0.6603,0.7626 -1.0681,1.23151 -1.4238,1.63741 -5.0422,6.27716 -5.9271,7.60002 -0.1563,0.23368 -0.7132,0.9003 -1.2376,1.48137 -0.5244,0.58107 -1.334,1.56767 -1.7992,2.19246 -0.4652,0.62478 -1.6177,2.11644 -2.5612,3.3148 -1.6506,2.09676 -5.5239,7.81878 -8.0593,11.90625 -0.6686,1.07769 -1.546,2.48445 -1.9496,3.12613 -0.4039,0.64169 -1.3744,2.31768 -2.1568,3.72444 -0.7825,1.40676 -1.7935,3.1266 -2.2469,3.82186 -0.4534,0.69527 -0.9868,1.67649 -1.1854,2.18051 -0.1987,0.504 -0.536,1.25666 -0.7496,1.67255 -1.4969,2.91424 -3.2808,7.9129 -4.0226,11.27137 -0.1264,0.57312 -0.3049,1.34046 -0.3964,1.70516 -1.0738,4.28096 -1.0368,31.97216 0.046,34.34866 0.324,0.71124 0.6487,0.64037 0.9915,-0.21641 z"
id="path21268" /><path
style="display:inline;fill:#812973;fill-opacity:1;stroke-width:0.71608"
d="m -1078.2395,-870.79523 c 0.7098,-7.13011 0.2636,-31.53813 -0.6564,-35.89493 -0.093,-0.44397 -2.6652,-7.38571 -5.8012,-13.55489 -2.6861,-5.28448 -3.3397,-6.45631 -4.5271,-8.11617 -1.2048,-1.68457 2.9545,4.03021 -4.8475,-7.74839 -1.1226,-1.69496 -2.3903,-3.50803 -2.817,-4.02904 -0.4268,-0.52102 -1.0546,-1.33099 -1.3953,-1.79991 -0.3407,-0.46892 -1.1011,-1.49202 -1.6897,-2.27354 -0.5886,-0.78154 -1.5187,-2.01778 -2.0669,-2.74722 -0.5481,-0.72944 -1.2546,-1.66727 -1.5699,-2.0841 -0.5298,-0.70052 -3.2613,-4.41584 -4.6346,-6.30389 -3.2216,-4.42897 -12.6092,-16.84587 -22.6349,-26.9449 -3.4964,-3.52219 -6.2044,-6.37773 -8.264,-8.71446 -1.0968,-1.24432 -2.8294,-2.94055 -4.5507,-4.45515 -1.8669,-1.64264 -2.084,-1.86732 -3.8454,-3.97872 -1.7014,-2.03966 -2.5666,-2.88156 -5.0597,-3.70856 -0.7207,-0.2391 -4.0827,-0.3408 -4.9339,-0.1491 -2.2674,0.5104 -3.5159,1.4734 -4.387,3.38401 -0.8915,1.9552 -0.9137,2.0117 -1.37,3.47936 -1.7346,3.28744 -4.1996,6.62796 -6.8429,9.33273 -0.7561,0.71659 -1.688,1.6013 -2.0708,1.96601 -0.3829,0.36471 -1.1544,1.04678 -1.7145,1.5157 -0.56,0.46892 -1.4063,1.2077 -1.8805,1.64173 -1.6622,1.52141 -3.0268,2.49602 -10.5875,7.56197 -3.0309,2.03077 -3.4789,2.31622 -4.3423,2.76676 -0.03,0.0156 -1.8058,1.23781 -4.1876,2.32977 -1.1149,0.59434 -2.9536,1.42519 -3.1541,1.42519 -0.084,0 -0.6941,0.25578 -1.3557,0.56838 -0.6617,0.31262 -1.275,0.5684 -1.3629,0.5684 -0.088,0 -0.4074,0.11936 -0.71,0.26524 -0.3026,0.14589 -1.0463,0.49593 -1.6527,0.77785 -1.1309,0.52574 -1.6101,1.12184 -1.3813,1.71808 0.2476,0.64512 3.4959,0.23053 7.8836,-1.00616 0.8857,-0.24965 1.9088,-0.5279 2.2735,-0.61833 1.4779,-0.3664 5.6207,-1.88788 7.768,-2.85287 4.4301,-1.99081 12.5209,-6.68979 15.0747,-8.75506 0.8508,-0.68804 2.5045,-1.97868 3.2086,-2.50409 1.2827,-0.95731 5.108,-4.65017 7.7679,-7.49895 3.5334,-3.78443 6.7745,-4.49663 9.3768,-2.06043 0.3639,0.34064 1.151,0.97546 1.7491,1.4107 1.4877,1.08252 5.5623,4.79321 7.6241,6.94318 2.8062,2.9263 6.459,6.712 9.6693,10.02119 4.595,4.73639 7.9926,8.59798 12.2446,13.91698 0.5831,0.72944 1.8606,2.22829 2.839,3.33078 0.9783,1.10249 3.0575,3.56479 4.6207,5.47176 1.563,1.90699 3.3702,4.1086 4.0161,4.89249 1.168,1.41764 5.5534,7.3874 6.2239,8.47213 0.1931,0.31262 1.5064,2.3035 2.9182,4.42418 1.4119,2.12069 2.8147,4.25214 3.1173,4.73658 0.3027,0.48443 0.9102,1.43496 1.3503,2.11228 1.6435,2.53022 2.9389,4.71285 4.2169,7.10487 0.7238,1.35466 1.5413,2.84667 1.8168,3.31559 1.0481,1.78405 1.8025,3.38892 2.5828,5.49443 0.4441,1.19835 0.9748,2.60511 1.1792,3.12614 1.6288,4.15067 2.5061,7.60396 3.297,12.9782 0.2684,1.82358 0.5194,3.52875 0.5578,3.78926 0.2807,1.90278 0.6632,6.91398 0.8317,10.89412 0.2169,5.1239 0.4391,7.07044 0.9083,7.95744 0.6754,1.27705 0.8975,0.91971 1.1778,-1.89462 z"
id="path21266" /><path
style="display:inline;fill:#812973;fill-opacity:1;stroke-width:0.71608"
d="m -1427.4952,-780.23842 c -0.066,-0.26406 -0.6617,-1.39373 -1.3231,-2.51039 -2.653,-4.47862 -6.2867,-11.44353 -7.6191,-14.60376 -0.3793,-0.89993 -1.0283,-2.40098 -1.4418,-3.33566 -0.9896,-2.23618 -2.1411,-5.22374 -3.3612,-8.72103 -0.5453,-1.56307 -1.1456,-3.2256 -1.3337,-3.69452 -0.5822,-1.45044 -1.2685,-3.60656 -2.9242,-9.18688 -0.4944,-1.66612 -0.9747,-3.17098 -1.0673,-3.34412 -0.4258,-0.79558 -1.5016,-4.0549 -1.5016,-4.54935 0,-0.30354 -0.1113,-0.82623 -0.2474,-1.16155 -0.136,-0.33532 -0.4006,-1.29173 -0.588,-2.12537 -0.1874,-0.83363 -0.4881,-2.06988 -0.6685,-2.74722 -0.7629,-2.86666 -1.3739,-5.26023 -1.5234,-5.96807 -0.088,-0.41681 -0.3295,-1.39729 -0.5363,-2.17882 -0.4471,-1.68801 -1.4363,-6.56762 -2.0164,-9.94681 -0.1431,-0.83363 -0.444,-2.32565 -0.6687,-3.3156 -0.2247,-0.98994 -0.5297,-2.3932 -0.6778,-3.11834 -0.148,-0.72516 -0.394,-1.74826 -0.5467,-2.27356 -0.1526,-0.52531 -0.4076,-1.93557 -0.5668,-3.13393 -0.1591,-1.19834 -0.4188,-2.90351 -0.5772,-3.78926 -0.2878,-1.61166 -0.744,-5.01553 -1.4343,-10.70466 -0.2022,-1.66727 -0.4921,-3.79872 -0.6441,-4.73657 -0.585,-3.60923 -1.3977,-10.37009 -1.6169,-13.45186 -0.6689,-9.40111 1.4723,-13.19364 7.7654,-13.75441 2.1828,-0.1945 5.4548,-0.76039 9.7573,-1.6874 0.9379,-0.20208 2.2594,-0.4594 2.9367,-0.57184 2.6892,-0.44638 6.2309,-1.16444 7.5785,-1.5365 2.4255,-0.66961 2.6028,-0.73581 6.6313,-2.47614 1.8571,-0.8023 2.9351,-1.4068 5.9207,-3.31997 1.6016,-1.02635 2.6472,-2.51542 1.7661,-2.51542 -0.348,0 -2.9272,0.69728 -6.1711,1.66843 -1.4068,0.42115 -2.9415,0.82206 -3.4103,0.89092 -0.4689,0.0689 -1.3216,0.24216 -1.8947,0.38513 -0.9428,0.23514 -2.3933,0.47904 -7.9575,1.33787 -10.0538,1.55184 -18.2839,1.72407 -26.1458,0.54715 -6.6202,-0.99103 -8.5693,0.97199 -7.6202,7.67506 0.1253,0.88573 0.3115,3.01719 0.4135,4.73656 0.3952,6.65485 1.0277,14.16501 1.4201,16.86219 0.5264,3.61822 1.2554,8.01942 1.355,8.18059 0.046,0.0736 0.2948,1.35375 0.554,2.8447 0.2591,1.49093 0.6862,3.86177 0.9489,5.26853 0.2627,1.40676 0.6054,3.32507 0.7616,4.26291 0.2589,1.55653 0.579,3.09324 1.598,7.67325 0.197,0.88574 0.4557,2.16461 0.5748,2.84195 0.1191,0.67732 0.2962,1.44546 0.3937,1.70696 0.1761,0.47274 0.7343,2.93123 1.5695,6.9136 0.2404,1.14625 0.6531,2.76615 0.9173,3.59979 0.2641,0.83364 0.6689,2.19777 0.8996,3.03141 0.2308,0.83364 0.5702,1.85676 0.7545,2.27362 0.1841,0.41686 0.3991,1.0563 0.4778,1.42097 0.079,0.36468 0.2641,0.89997 0.412,1.18952 0.148,0.28955 0.6399,1.95209 1.0933,3.69452 0.4533,1.74244 1.054,3.76488 1.3348,4.49431 0.281,0.72943 0.8937,2.47723 1.3617,3.88399 1.0628,3.19377 3.3308,9.04569 4.0485,10.44531 0.087,0.16998 0.5568,1.27834 1.0435,2.46303 0.4868,1.18467 0.9471,2.23922 1.0228,2.34342 0.076,0.1042 0.2529,0.48786 0.3939,0.85258 0.8084,2.09342 3.8715,8.00474 5.4128,10.44628 0.2514,0.39847 0.4573,0.77535 0.4573,0.83747 0,0.0622 0.4065,0.7246 0.9033,1.47211 0.4969,0.74753 1.008,1.57228 1.1358,1.83278 0.1277,0.26052 0.6705,1.15573 1.206,1.98937 0.5357,0.83364 1.1902,1.89936 1.4546,2.36828 0.7343,1.30211 5.185,7.96679 6.3853,9.56157 1.6458,2.18686 3.7314,3.6869 3.4242,2.46288 z"
id="path21264" /><path
style="display:inline;fill:#812973;fill-opacity:1;stroke-width:0.71608"
d="m -1194.7853,-780.1521 c 2.1923,-2.10664 6.1213,-7.42853 8.2772,-11.21191 0.9164,-1.6081 1.2324,-2.11477 3.3378,-5.35124 3.1986,-4.91726 4.0506,-6.37827 6.2424,-10.70465 1.2935,-2.55301 2.5372,-5.06813 2.7642,-5.58915 0.2267,-0.52102 0.6849,-1.58675 1.0181,-2.36829 0.3332,-0.78154 0.7553,-1.76201 0.938,-2.17883 1.3324,-3.0396 2.8522,-6.98255 4.5276,-11.74669 1.0884,-3.09505 2.9791,-9.18527 3.1469,-10.13627 0.065,-0.36471 0.2725,-1.17467 0.4626,-1.79989 0.4298,-1.41305 1.4193,-4.70962 1.905,-6.34702 0.2009,-0.67732 0.4933,-1.78568 0.6497,-2.46301 0.1564,-0.67733 0.5031,-2.12672 0.7703,-3.22087 0.6395,-2.61793 2.0454,-8.97072 2.8511,-12.88348 0.354,-1.71938 0.7344,-3.42453 0.8452,-3.78926 0.4172,-1.37282 2.0398,-10.61296 2.1958,-12.50455 0.056,-0.67733 0.3076,-2.38249 0.5593,-3.78925 0.2515,-1.40677 0.5434,-3.36771 0.6485,-4.35766 0.1049,-0.98994 0.3567,-2.78036 0.5593,-3.97872 3.8712,-22.89397 2.8792,-26.41354 -6.9973,-24.82731 -6.2063,0.99676 -20.7722,0.69409 -27.3774,-0.56888 -1.0197,-0.19496 -3.9937,-0.74938 -6.4418,-1.20083 -0.8336,-0.15375 -2.6666,-0.45883 -4.0733,-0.67797 -1.4069,-0.21915 -2.8136,-0.47608 -3.1262,-0.57097 -0.6394,-0.19409 -3.6538,-0.86857 -4.593,-1.0277 -0.5731,-0.0971 -0.5986,-0.079 -0.3807,0.26981 0.1284,0.20564 0.2862,0.37389 0.3507,0.37389 1.2035,0 2.0655,3.24173 19.3064,8.15076 4.2795,1.20844 9.0922,1.94859 15.3141,2.35516 3.6958,0.2415 5.2294,0.8093 6.5163,2.41246 2.4232,3.01901 2.6974,7.10045 1.3091,19.47976 -0.2045,1.82358 -0.4691,4.38133 -0.588,5.68389 -0.455,4.98503 -1.3988,11.55273 -2.287,15.91489 -0.3077,1.51097 -0.8137,4.0261 -1.1245,5.58916 -0.5784,2.90983 -1.8205,8.92228 -2.368,11.4625 -0.1684,0.78154 -0.3837,1.87182 -0.4782,2.42286 -0.221,1.28722 -0.9407,4.61225 -1.0302,4.75902 -0.038,0.0618 -0.3814,1.64709 -0.7638,3.52278 -0.8278,4.06011 -1.1576,5.29252 -3.4222,12.78873 -0.1417,0.46894 -0.5258,1.74781 -0.8534,2.84195 -0.3277,1.09416 -0.7524,2.45829 -0.9438,3.03141 -0.1912,0.57312 -0.6471,1.97988 -1.0131,3.12614 -0.366,1.14625 -1.1018,3.27771 -1.6353,4.73657 -0.5334,1.45886 -1.3009,3.63295 -1.7055,4.83131 -2.2837,6.76422 -3.7336,10.3713 -5.854,14.56423 -1.6135,3.19059 -1.4241,2.87254 -4.4717,7.5082 -0.959,1.45886 -2.0711,3.20665 -2.471,3.88399 -0.4,0.67732 -0.7751,1.27413 -0.8336,1.32623 -0.1404,0.12514 -0.7091,1.08138 -1.0883,1.83004 -0.673,1.32864 0.2121,1.59478 1.4257,0.42866 z"
id="path21262" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1273.7037,-977.54923 c -0.772,-0.47069 -1.0237,-0.8741 -1.7784,-2.85028 -2.3054,-6.03708 -11.9745,-9.16172 -18.1556,-5.8672 -2.0578,1.09679 -4.9207,4.85312 -5.3221,6.01429 -0.6057,1.75171 -1.9555,3.17543 -3.3116,1.81937 -0.529,-0.52902 -0.021,-3.8741 0.9303,-6.13066 5.9705,-14.15649 23.3562,-13.47962 29.0968,1.13279 0.4222,1.0744 1.1272,4.11765 1.1325,4.88772 0.01,1.10354 -1.4779,1.67321 -2.5919,0.99397 z"
id="path21275" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1354.8435,-977.87889 c -1.6707,-1.43007 -0.6421,-5.87088 2.361,-10.19386 7.0117,-10.09294 18.5658,-9.86933 25.6798,0.497 2.0724,3.01978 3.6886,8.04135 2.877,8.93823 -1.0773,1.19052 -2.6788,0.56325 -3.0747,-0.28432 -0.5216,-1.11631 -2.4548,-4.72705 -4.3919,-6.25286 -7.586,-5.97524 -17.0614,-3.29825 -20.7104,5.85112 -0.6548,1.64176 -1.0982,2.14494 -1.8903,2.14494 -0.017,0 -0.4005,-0.31511 -0.8505,-0.70025 z"
id="path21274" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1314.6853,-934.56573 c 0,0.0665 -3.1068,-0.30747 -4.912,-0.52467 -0.1929,-0.0232 -0.9132,-0.13398 -1.6947,-0.29027 -1.2597,-0.25194 -1.3953,-0.2116 -3.1507,-0.83734 -0.3125,-0.11144 -1.421,-0.47049 -2.463,-0.7979 -9.4379,-2.96537 -18.6541,-11.35821 -21.1051,-19.21954 -1.2012,-3.85268 -0.8806,-4.07451 3.5747,-2.47362 0.7788,0.27982 2.1003,0.58344 2.9366,0.67473 0.8365,0.0913 1.8193,0.25162 2.184,0.3563 0.3647,0.10469 1.6436,0.31785 2.8419,0.47371 1.1984,0.15586 2.7757,0.40839 3.5052,0.56117 0.7294,0.15278 2.3283,0.37049 3.5529,0.48379 1.2247,0.1133 2.8445,0.33866 3.5997,0.5008 0.7552,0.16213 2.0552,0.32707 2.8888,0.36656 0.8338,0.0394 2.283,0.12906 3.2209,0.19907 4.655,0.34753 16.2868,0.0317 20.9357,-0.56822 1.1983,-0.15468 2.7329,-0.28504 3.4103,-0.28969 0.6773,-0.005 1.9562,-0.13871 2.8419,-0.29788 0.8858,-0.15919 3.0598,-0.53911 4.8314,-0.84429 2.3425,-0.40355 3.7375,-0.75702 5.1155,-1.29615 2.3947,-0.93695 2.9873,-1.06659 3.5413,-0.77474 2.9049,1.53048 -3.8173,13.28525 -9.9576,17.412 -0.5071,0.34075 -1.4846,1.04554 -2.1725,1.56619 -1.9371,1.46617 -5.0104,2.90117 -7.7997,3.64196 -0.6774,0.17988 -1.5749,0.44618 -1.9945,0.59178 -0.4195,0.1456 -1.6558,0.44242 -2.7472,0.6596 -1.7204,0.34234 -2.8441,0.46959 -6.9103,0.78248 -0.4169,0.0321 -2.2499,0.007 -4.0735,-0.0558 z"
id="path21273" /><path
id="path21276-14"
style="display:inline;fill:#6d2361;fill-opacity:1;stroke-width:0.720242"
d="m -1245.0685,-1055.6205 a 86.1772,81.849525 0 0 1 -25.2215,20.4684 c 1.3705,1.0078 2.5102,1.8362 3.7749,2.8552 0.5487,0.4422 1.3363,1.1592 1.8065,1.6 0.4702,0.441 1.0743,0.9732 1.9413,1.6682 1.6057,1.2868 2.1849,1.7488 2.7438,2.126 1.2474,0.8413 1.6524,1.6882 2.4775,0.8079 v 0 c 0.6031,-0.6437 0.9274,-1.0717 1.8354,-2.4244 0.8999,-1.3407 4.7148,-8.3481 5.1155,-9.3968 0.141,-0.3689 0.319,-0.7571 0.3957,-0.8625 0.077,-0.1054 0.447,-1.0111 0.8233,-2.0126 0.3763,-1.0014 0.8464,-2.252 1.0446,-2.7792 1.3959,-3.7118 2.4649,-7.8224 3.263,-12.0539 z m -133.0717,2.3338 c 0.8022,4.0023 1.9581,8.1841 2.9725,10.4828 0.2326,0.527 0.526,1.3033 0.652,1.725 0.3682,1.2319 0.7939,2.2039 1.6375,3.7375 0.4348,0.7906 0.9873,1.9318 1.2278,2.5356 0.2404,0.6038 0.9998,1.8114 1.6878,2.6833 1.5618,1.9797 2.3012,3.3632 2.9503,4.1981 1.6195,2.0828 1.8125,2.3532 4.1571,-0.086 1.0457,-1.0876 2.7113,-2.6246 3.7012,-3.4157 0.99,-0.791 1.9279,-1.5619 2.0841,-1.713 0.4811,-0.4653 1.4143,-1.1458 2.534,-1.8984 a 86.1772,81.849525 0 0 1 -23.6043,-18.2495 z" /><path
id="path21276-1"
style="display:inline;fill:#985289;fill-opacity:1;stroke-width:0.71608"
d="m -1311.1821,-1147.2592 c -3.2889,3e-4 -6.5629,0.2086 -8.2398,0.6244 -0.5732,0.1427 -1.4257,0.352 -1.8946,0.4651 -1.9029,0.459 -5.2315,1.3853 -5.5514,1.5449 -0.1878,0.093 -0.9749,0.336 -1.7488,0.5388 -0.7741,0.2026 -1.7713,0.5687 -2.2163,0.8131 -0.445,0.2442 -1.421,0.6748 -2.1688,0.9568 -0.7479,0.2821 -1.4149,0.7081 -1.5986,0.8004 -1.0377,0.5217 -2.6866,1.1878 -3.5783,1.7038 -0.469,0.2712 -1.5773,0.8797 -2.4631,1.3521 -0.8857,0.4724 -1.7382,0.9637 -1.8946,1.092 -0.1563,0.1282 -1.3925,0.9408 -2.7473,1.8058 -2.5194,1.6087 -4.4043,3.0459 -6.909,5.2684 -0.7789,0.6911 -1.8449,1.6336 -2.369,2.0943 -1.4987,1.3175 -5.6597,5.6042 -7.4852,7.7114 -3.0994,3.5774 -6.0728,8.0025 -8.1759,12.0368 0.5837,-0.6509 1.1617,-1.3213 1.7548,-1.9282 2.0589,-2.1073 6.752,-6.394 8.4421,-7.7114 0.5912,-0.4607 1.7935,-1.4034 2.6718,-2.0945 2.8249,-2.2223 4.9508,-3.6595 7.7924,-5.2683 1.5279,-0.865 2.9218,-1.6777 3.098,-1.8058 0.1763,-0.1282 1.1381,-0.6197 2.1371,-1.092 0.9989,-0.4725 2.249,-1.0808 2.7779,-1.3522 1.0056,-0.516 2.865,-1.182 4.0353,-1.7036 0.2072,-0.092 0.9593,-0.5183 1.8028,-0.8004 0.8435,-0.2821 1.9442,-0.7128 2.446,-0.957 0.5019,-0.2443 1.6268,-0.6102 2.4997,-0.813 0.8729,-0.2027 1.7606,-0.4451 1.9724,-0.5388 0.3607,-0.1596 4.115,-1.0859 6.2611,-1.5449 0.5289,-0.1131 1.4902,-0.3225 2.1367,-0.4651 3.7823,-0.8314 14.7699,-0.8337 18.6867,0 5.0249,1.125 9.8346,2.5794 14.4992,4.5533 11.4814,4.1533 22.6764,11.7898 33.1545,22.6161 0.3957,0.4087 0.7633,0.8274 1.1441,1.2415 -2.0368,-3.9403 -4.5999,-7.7496 -7.6894,-11.3498 -9.2906,-10.8261 -19.2173,-18.463 -29.3975,-22.6163 -4.1359,-1.9739 -8.4004,-3.4283 -12.8557,-4.5533 -1.7365,-0.4169 -5.0404,-0.6246 -8.3293,-0.6244 z" /></g></g></svg>

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

BIN
img/server-and-env-page.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
img/sql_injection.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
img/users-and-secrets.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

47
kubernetes/README.md Normal file
View File

@@ -0,0 +1,47 @@
### Kubernetes
Apply all manifests with:
```bash
kubectl apply -f https://raw.githubusercontent.com/BlessedRebuS/Krawl/refs/heads/main/kubernetes/krawl-all-in-one-deploy.yaml
```
Or clone the repo and apply the manifest:
```bash
kubectl apply -f kubernetes/krawl-all-in-one-deploy.yaml
```
Access the deception server:
```bash
kubectl get svc krawl-server -n krawl-system
```
Once the EXTERNAL-IP is assigned, access your deception server at `http://<EXTERNAL-IP>:5000`
### Retrieving Dashboard Path
Check server startup logs or get the secret with
```bash
kubectl get secret krawl-server -n krawl-system \
-o jsonpath='{.data.dashboard-path}' | base64 -d && echo
```
### From Source (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` and access the dashboard at `http://localhost:5000/<dashboard-secret-path>`

View File

@@ -4,325 +4,21 @@ kind: Namespace
metadata:
name: krawl-system
---
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
---
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
]
}
---
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"
volumes:
- name: wordlists
configMap:
name: krawl-wordlists
---
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
---
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
---
# Source: krawl-chart/templates/network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: krawl-network-policy
name: krawl
namespace: krawl-system
labels:
app.kubernetes.io/name: krawl
app.kubernetes.io/instance: krawl
app.kubernetes.io/version: "1.0.0"
spec:
podSelector:
matchLabels:
app: krawl-server
app.kubernetes.io/name: krawl
app.kubernetes.io/instance: krawl
policyTypes:
- Ingress
- Egress
@@ -333,40 +29,201 @@ spec:
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
port: 5000
- port: 5000
protocol: TCP
egress:
- to:
- ports:
- protocol: TCP
- protocol: UDP
to:
- namespaceSelector: {}
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
- protocol: UDP
---
# Optional: HorizontalPodAutoscaler for auto-scaling
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
# Source: krawl-chart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: krawl-hpa
name: krawl-config
namespace: krawl-system
labels:
app.kubernetes.io/name: krawl
app.kubernetes.io/instance: krawl
app.kubernetes.io/version: "1.0.0"
data:
config.yaml: |
# Krawl Honeypot Configuration
server:
port: 5000
delay: 100
links:
min_length: 5
max_length: 15
min_per_page: 10
max_per_page: 15
char_space: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
max_counter: 10
canary:
token_url: null
token_tries: 10
dashboard:
secret_path: null
database:
path: "data/krawl.db"
retention_days: 30
behavior:
probability_error_codes: 0
analyzer:
http_risky_methods_threshold: 0.1
violated_robots_threshold: 0.1
uneven_request_timing_threshold: 0.5
uneven_request_timing_time_window_seconds: 300
user_agents_used_threshold: 2
attack_urls_threshold: 1
crawl:
infinite_pages_for_malicious: true
max_pages_limit: 250
ban_duration_seconds: 600
---
# Source: krawl-chart/templates/wordlists-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: krawl-wordlists
namespace: krawl-system
labels:
app.kubernetes.io/name: krawl
app.kubernetes.io/instance: krawl
app.kubernetes.io/version: "1.0.0"
data:
wordlists.json: |
{"api_keys":{"prefixes":["sk_live_","sk_test_","api_","key_","token_","access_","secret_","prod_",""]},"applications":{"names":["WebApp","API Gateway","Dashboard","Admin Panel","CMS","Portal","Manager","Console","Control Panel","Backend"]},"databases":{"hosts":["localhost","db.internal","mysql.local","postgres.internal","127.0.0.1","db-server-01","database.prod","sql.company.com"],"names":["production","prod_db","main_db","app_database","users_db","customer_data","analytics","staging_db","dev_database","wordpress","ecommerce","crm_db","inventory"]},"directory_listing":{"directories":["uploads/","backups/","logs/","temp/","cache/","private/","config/","admin/","database/","backup/","old/","archive/",".git/","keys/","credentials/"],"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"]},"emails":{"domains":["example.com","company.com","localhost.com","test.com","domain.com","corporate.com","internal.net","enterprise.com","business.org"]},"error_codes":[400,401,403,404,500,502,503],"passwords":{"prefixes":["P@ssw0rd","Passw0rd","Admin","Secret","Welcome","System","Database","Secure","Master","Root"],"simple":["test","demo","temp","change","password","admin","letmein","welcome","default","sample"]},"server_headers":["Apache/2.2.22 (Ubuntu)","nginx/1.18.0","Microsoft-IIS/10.0","LiteSpeed","Caddy","Gunicorn/20.0.4","uvicorn/0.13.4","Express","Flask/1.1.2","Django/3.1"],"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"]},"users":{"roles":["Administrator","Developer","Manager","User","Guest","Moderator","Editor","Viewer","Analyst","Support"]}}
---
# Source: krawl-chart/templates/pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: krawl-db
namespace: krawl-system
labels:
app.kubernetes.io/name: krawl
app.kubernetes.io/instance: krawl
app.kubernetes.io/version: "1.0.0"
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
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
# Source: krawl-chart/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: krawl
namespace: krawl-system
labels:
app.kubernetes.io/name: krawl
app.kubernetes.io/instance: krawl
app.kubernetes.io/version: "1.0.0"
spec:
type: LoadBalancer
externalTrafficPolicy: Local
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800
ports:
- port: 5000
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/name: krawl
app.kubernetes.io/instance: krawl
---
# Source: krawl-chart/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: krawl
namespace: krawl-system
labels:
app.kubernetes.io/name: krawl
app.kubernetes.io/instance: krawl
app.kubernetes.io/version: "1.0.0"
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: krawl
app.kubernetes.io/instance: krawl
template:
metadata:
labels:
app.kubernetes.io/name: krawl
app.kubernetes.io/instance: krawl
spec:
containers:
- name: krawl-chart
image: "ghcr.io/blessedrebus/krawl:1.0.0"
imagePullPolicy: Always
ports:
- name: http
containerPort: 5000
protocol: TCP
env:
- name: CONFIG_LOCATION
value: "config.yaml"
volumeMounts:
- name: config
mountPath: /app/config.yaml
subPath: config.yaml
readOnly: true
- name: wordlists
mountPath: /app/wordlists.json
subPath: wordlists.json
readOnly: true
- name: database
mountPath: /app/data
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 64Mi
volumes:
- name: config
configMap:
name: krawl-config
- name: wordlists
configMap:
name: krawl-wordlists
- name: database
persistentVolumeClaim:
claimName: krawl-db
---
# Source: krawl-chart/templates/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: krawl
namespace: krawl-system
labels:
app.kubernetes.io/name: krawl
app.kubernetes.io/instance: krawl
app.kubernetes.io/version: "1.0.0"
spec:
ingressClassName: traefik
rules:
- host: "krawl.example.com"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: krawl
port:
number: 5000

View File

@@ -1,17 +1,44 @@
# Source: krawl-chart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: krawl-config
namespace: krawl-system
labels:
app.kubernetes.io/name: krawl
app.kubernetes.io/instance: krawl
app.kubernetes.io/version: "1.0.0"
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"
SERVER_HEADER: "Apache/2.2.22 (Ubuntu)"
# CANARY_TOKEN_URL: set-your-canary-token-url-here
config.yaml: |
# Krawl Honeypot Configuration
server:
port: 5000
delay: 100
links:
min_length: 5
max_length: 15
min_per_page: 10
max_per_page: 15
char_space: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
max_counter: 10
canary:
token_url: null
token_tries: 10
dashboard:
secret_path: null
database:
path: "data/krawl.db"
retention_days: 30
behavior:
probability_error_codes: 0
analyzer:
http_risky_methods_threshold: 0.1
violated_robots_threshold: 0.1
uneven_request_timing_threshold: 0.5
uneven_request_timing_time_window_seconds: 300
user_agents_used_threshold: 2
attack_urls_threshold: 1
crawl:
infinite_pages_for_malicious: true
max_pages_limit: 250
ban_duration_seconds: 600

View File

@@ -1,44 +1,61 @@
# Source: krawl-chart/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: krawl-server
name: krawl
namespace: krawl-system
labels:
app: krawl-server
app.kubernetes.io/name: krawl
app.kubernetes.io/instance: krawl
app.kubernetes.io/version: "1.0.0"
spec:
replicas: 1
selector:
matchLabels:
app: krawl-server
app.kubernetes.io/name: krawl
app.kubernetes.io/instance: krawl
template:
metadata:
labels:
app: krawl-server
app.kubernetes.io/name: krawl
app.kubernetes.io/instance: krawl
spec:
containers:
- name: krawl
image: ghcr.io/blessedrebus/krawl:latest
- name: krawl-chart
image: "ghcr.io/blessedrebus/krawl:1.0.0"
imagePullPolicy: Always
ports:
- containerPort: 5000
name: http
- name: http
containerPort: 5000
protocol: TCP
envFrom:
- configMapRef:
name: krawl-config
env:
- name: CONFIG_LOCATION
value: "config.yaml"
volumeMounts:
- name: config
mountPath: /app/config.yaml
subPath: config.yaml
readOnly: true
- name: wordlists
mountPath: /app/wordlists.json
subPath: wordlists.json
readOnly: true
- name: database
mountPath: /app/data
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 64Mi
volumes:
- name: config
configMap:
name: krawl-config
- name: wordlists
configMap:
name: krawl-wordlists
- name: database
persistentVolumeClaim:
claimName: krawl-db

View File

@@ -1,26 +0,0 @@
# 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

View File

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

View File

@@ -5,6 +5,7 @@ resources:
- namespace.yaml
- configmap.yaml
- wordlists-configmap.yaml
- pvc.yaml
- deployment.yaml
- service.yaml
- network-policy.yaml

View File

@@ -1,12 +1,18 @@
# Source: krawl-chart/templates/network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: krawl-network-policy
name: krawl
namespace: krawl-system
labels:
app.kubernetes.io/name: krawl
app.kubernetes.io/instance: krawl
app.kubernetes.io/version: "1.0.0"
spec:
podSelector:
matchLabels:
app: krawl-server
app.kubernetes.io/name: krawl
app.kubernetes.io/instance: krawl
policyTypes:
- Ingress
- Egress
@@ -17,13 +23,13 @@ spec:
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
port: 5000
- port: 5000
protocol: TCP
egress:
- to:
- ports:
- protocol: TCP
- protocol: UDP
to:
- namespaceSelector: {}
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
- protocol: UDP

View File

@@ -0,0 +1,16 @@
# Source: krawl-chart/templates/pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: krawl-db
namespace: krawl-system
labels:
app.kubernetes.io/name: krawl
app.kubernetes.io/instance: krawl
app.kubernetes.io/version: "1.0.0"
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi

View File

@@ -1,16 +1,25 @@
# Source: krawl-chart/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: krawl-server
name: krawl
namespace: krawl-system
labels:
app: krawl-server
app.kubernetes.io/name: krawl
app.kubernetes.io/instance: krawl
app.kubernetes.io/version: "1.0.0"
spec:
type: LoadBalancer
externalTrafficPolicy: Local
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800
ports:
- port: 5000
targetPort: 5000
targetPort: http
protocol: TCP
name: http
selector:
app: krawl-server
app.kubernetes.io/name: krawl
app.kubernetes.io/instance: krawl

View File

@@ -1,205 +1,13 @@
# Source: krawl-chart/templates/wordlists-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: krawl-wordlists
namespace: krawl-system
labels:
app.kubernetes.io/name: krawl
app.kubernetes.io/instance: krawl
app.kubernetes.io/version: "1.0.0"
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
]
}
{"api_keys":{"prefixes":["sk_live_","sk_test_","api_","key_","token_","access_","secret_","prod_",""]},"applications":{"names":["WebApp","API Gateway","Dashboard","Admin Panel","CMS","Portal","Manager","Console","Control Panel","Backend"]},"databases":{"hosts":["localhost","db.internal","mysql.local","postgres.internal","127.0.0.1","db-server-01","database.prod","sql.company.com"],"names":["production","prod_db","main_db","app_database","users_db","customer_data","analytics","staging_db","dev_database","wordpress","ecommerce","crm_db","inventory"]},"directory_listing":{"directories":["uploads/","backups/","logs/","temp/","cache/","private/","config/","admin/","database/","backup/","old/","archive/",".git/","keys/","credentials/"],"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"]},"emails":{"domains":["example.com","company.com","localhost.com","test.com","domain.com","corporate.com","internal.net","enterprise.com","business.org"]},"error_codes":[400,401,403,404,500,502,503],"passwords":{"prefixes":["P@ssw0rd","Passw0rd","Admin","Secret","Welcome","System","Database","Secure","Master","Root"],"simple":["test","demo","temp","change","password","admin","letmein","welcome","default","sample"]},"server_headers":["Apache/2.2.22 (Ubuntu)","nginx/1.18.0","Microsoft-IIS/10.0","LiteSpeed","Caddy","Gunicorn/20.0.4","uvicorn/0.13.4","Express","Flask/1.1.2","Django/3.1"],"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"]},"users":{"roles":["Administrator","Developer","Manager","User","Guest","Moderator","Editor","Viewer","Analyst","Support"]}}

13
requirements.txt Normal file
View File

@@ -0,0 +1,13 @@
# Krawl Honeypot Dependencies
# Install with: pip install -r requirements.txt
# Configuration
PyYAML>=6.0
# Database ORM
SQLAlchemy>=2.0.0,<3.0.0
# Scheduling
APScheduler>=3.11.2
requests>=2.32.5

342
src/analyzer.py Normal file
View File

@@ -0,0 +1,342 @@
#!/usr/bin/env python3
from sqlalchemy import select
from typing import Optional
from database import get_database, DatabaseManager
from zoneinfo import ZoneInfo
from pathlib import Path
from datetime import datetime, timedelta
import re
import urllib.parse
from wordlists import get_wordlists
from config import get_config
from logger import get_app_logger
import requests
"""
Functions for user activity analysis
"""
app_logger = get_app_logger()
class Analyzer:
"""
Analyzes users activity and produces aggregated insights
"""
def __init__(self, db_manager: Optional[DatabaseManager] = None):
"""
Initialize the analyzer.
Args:
db_manager: Optional DatabaseManager for persistence.
If None, will use the global singleton.
"""
self._db_manager = db_manager
@property
def db(self) -> Optional[DatabaseManager]:
"""
Get the database manager, lazily initializing if needed.
Returns:
DatabaseManager instance or None if not available
"""
if self._db_manager is None:
try:
self._db_manager = get_database()
except Exception:
pass
return self._db_manager
# def infer_user_category(self, ip: str) -> str:
# config = get_config()
# http_risky_methods_threshold = config.http_risky_methods_threshold
# violated_robots_threshold = config.violated_robots_threshold
# uneven_request_timing_threshold = config.uneven_request_timing_threshold
# user_agents_used_threshold = config.user_agents_used_threshold
# attack_urls_threshold = config.attack_urls_threshold
# uneven_request_timing_time_window_seconds = config.uneven_request_timing_time_window_seconds
# app_logger.debug(f"http_risky_methods_threshold: {http_risky_methods_threshold}")
# score = {}
# score["attacker"] = {"risky_http_methods": False, "robots_violations": False, "uneven_request_timing": False, "different_user_agents": False, "attack_url": False}
# score["good_crawler"] = {"risky_http_methods": False, "robots_violations": False, "uneven_request_timing": False, "different_user_agents": False, "attack_url": False}
# score["bad_crawler"] = {"risky_http_methods": False, "robots_violations": False, "uneven_request_timing": False, "different_user_agents": False, "attack_url": False}
# score["regular_user"] = {"risky_http_methods": False, "robots_violations": False, "uneven_request_timing": False, "different_user_agents": False, "attack_url": False}
# #1-3 low, 4-6 mid, 7-9 high, 10-20 extreme
# weights = {
# "attacker": {
# "risky_http_methods": 6,
# "robots_violations": 4,
# "uneven_request_timing": 3,
# "different_user_agents": 8,
# "attack_url": 15
# },
# "good_crawler": {
# "risky_http_methods": 1,
# "robots_violations": 0,
# "uneven_request_timing": 0,
# "different_user_agents": 0,
# "attack_url": 0
# },
# "bad_crawler": {
# "risky_http_methods": 2,
# "robots_violations": 7,
# "uneven_request_timing": 0,
# "different_user_agents": 5,
# "attack_url": 5
# },
# "regular_user": {
# "risky_http_methods": 0,
# "robots_violations": 0,
# "uneven_request_timing": 8,
# "different_user_agents": 3,
# "attack_url": 0
# }
# }
# accesses = self.db.get_access_logs(ip_filter = ip, limit=1000)
# total_accesses_count = len(accesses)
# if total_accesses_count <= 0:
# return
# # Set category as "unknown" for the first 5 requests
# if total_accesses_count < 3:
# category = "unknown"
# analyzed_metrics = {}
# category_scores = {"attacker": 0, "good_crawler": 0, "bad_crawler": 0, "regular_user": 0, "unknown": 0}
# last_analysis = datetime.now(tz=ZoneInfo('UTC'))
# self._db_manager.update_ip_stats_analysis(ip, analyzed_metrics, category, category_scores, last_analysis)
# return 0
# #--------------------- HTTP Methods ---------------------
# get_accesses_count = len([item for item in accesses if item["method"] == "GET"])
# post_accesses_count = len([item for item in accesses if item["method"] == "POST"])
# put_accesses_count = len([item for item in accesses if item["method"] == "PUT"])
# delete_accesses_count = len([item for item in accesses if item["method"] == "DELETE"])
# head_accesses_count = len([item for item in accesses if item["method"] == "HEAD"])
# options_accesses_count = len([item for item in accesses if item["method"] == "OPTIONS"])
# patch_accesses_count = len([item for item in accesses if item["method"] == "PATCH"])
# if total_accesses_count > http_risky_methods_threshold:
# http_method_attacker_score = (post_accesses_count + put_accesses_count + delete_accesses_count + options_accesses_count + patch_accesses_count) / total_accesses_count
# else:
# http_method_attacker_score = 0
# #print(f"HTTP Method attacker score: {http_method_attacker_score}")
# if http_method_attacker_score >= http_risky_methods_threshold:
# score["attacker"]["risky_http_methods"] = True
# score["good_crawler"]["risky_http_methods"] = False
# score["bad_crawler"]["risky_http_methods"] = True
# score["regular_user"]["risky_http_methods"] = False
# else:
# score["attacker"]["risky_http_methods"] = False
# score["good_crawler"]["risky_http_methods"] = True
# score["bad_crawler"]["risky_http_methods"] = False
# score["regular_user"]["risky_http_methods"] = False
# #--------------------- Robots Violations ---------------------
# #respect robots.txt and login/config pages access frequency
# robots_disallows = []
# robots_path = Path(__file__).parent / "templates" / "html" / "robots.txt"
# with open(robots_path, "r") as f:
# for line in f:
# line = line.strip()
# if not line:
# continue
# parts = line.split(":")
# if parts[0] == "Disallow":
# parts[1] = parts[1].rstrip("/")
# #print(f"DISALLOW {parts[1]}")
# robots_disallows.append(parts[1].strip())
# #if 0 100% sure is good crawler, if >10% of robots violated is bad crawler or attacker
# violated_robots_count = len([item for item in accesses if any(item["path"].rstrip("/").startswith(disallow) for disallow in robots_disallows)])
# #print(f"Violated robots count: {violated_robots_count}")
# if total_accesses_count > 0:
# violated_robots_ratio = violated_robots_count / total_accesses_count
# else:
# violated_robots_ratio = 0
# if violated_robots_ratio >= violated_robots_threshold:
# score["attacker"]["robots_violations"] = True
# score["good_crawler"]["robots_violations"] = False
# score["bad_crawler"]["robots_violations"] = True
# score["regular_user"]["robots_violations"] = False
# else:
# score["attacker"]["robots_violations"] = False
# score["good_crawler"]["robots_violations"] = False
# score["bad_crawler"]["robots_violations"] = False
# score["regular_user"]["robots_violations"] = False
# #--------------------- Requests Timing ---------------------
# #Request rate and timing: steady, throttled, polite vs attackers' bursty, aggressive, or oddly rhythmic behavior
# timestamps = [datetime.fromisoformat(item["timestamp"]) for item in accesses]
# now_utc = datetime.now(tz=ZoneInfo('UTC'))
# timestamps = [ts for ts in timestamps if now_utc - ts <= timedelta(seconds=uneven_request_timing_time_window_seconds)]
# timestamps = sorted(timestamps, reverse=True)
# time_diffs = []
# for i in range(0, len(timestamps)-1):
# diff = (timestamps[i] - timestamps[i+1]).total_seconds()
# time_diffs.append(diff)
# mean = 0
# variance = 0
# std = 0
# cv = 0
# if time_diffs:
# mean = sum(time_diffs) / len(time_diffs)
# variance = sum((x - mean) ** 2 for x in time_diffs) / len(time_diffs)
# std = variance ** 0.5
# cv = std/mean
# app_logger.debug(f"Mean: {mean} - Variance {variance} - Standard Deviation {std} - Coefficient of Variation: {cv}")
# if cv >= uneven_request_timing_threshold:
# score["attacker"]["uneven_request_timing"] = True
# score["good_crawler"]["uneven_request_timing"] = False
# score["bad_crawler"]["uneven_request_timing"] = False
# score["regular_user"]["uneven_request_timing"] = True
# else:
# score["attacker"]["uneven_request_timing"] = False
# score["good_crawler"]["uneven_request_timing"] = False
# score["bad_crawler"]["uneven_request_timing"] = False
# score["regular_user"]["uneven_request_timing"] = False
# #--------------------- Different User Agents ---------------------
# #Header Quality and Consistency: Crawlers tend to use complete and consistent headers, attackers might miss, fake, or change headers
# user_agents_used = [item["user_agent"] for item in accesses]
# user_agents_used = list(dict.fromkeys(user_agents_used))
# #print(f"User agents used: {user_agents_used}")
# if len(user_agents_used) >= user_agents_used_threshold:
# score["attacker"]["different_user_agents"] = True
# score["good_crawler"]["different_user_agents"] = False
# score["bad_crawler"]["different_user_agentss"] = True
# score["regular_user"]["different_user_agents"] = False
# else:
# score["attacker"]["different_user_agents"] = False
# score["good_crawler"]["different_user_agents"] = False
# score["bad_crawler"]["different_user_agents"] = False
# score["regular_user"]["different_user_agents"] = False
# #--------------------- Attack URLs ---------------------
# attack_urls_found_list = []
# wl = get_wordlists()
# if wl.attack_patterns:
# queried_paths = [item["path"] for item in accesses]
# for queried_path in queried_paths:
# # URL decode the path to catch encoded attacks
# try:
# decoded_path = urllib.parse.unquote(queried_path)
# # Double decode to catch double-encoded attacks
# decoded_path_twice = urllib.parse.unquote(decoded_path)
# except Exception:
# decoded_path = queried_path
# decoded_path_twice = queried_path
# for name, pattern in wl.attack_patterns.items():
# # Check original, decoded, and double-decoded paths
# if (re.search(pattern, queried_path, re.IGNORECASE) or
# re.search(pattern, decoded_path, re.IGNORECASE) or
# re.search(pattern, decoded_path_twice, re.IGNORECASE)):
# attack_urls_found_list.append(f"{name}: {pattern}")
# #remove duplicates
# attack_urls_found_list = set(attack_urls_found_list)
# attack_urls_found_list = list(attack_urls_found_list)
# if len(attack_urls_found_list) > attack_urls_threshold:
# score["attacker"]["attack_url"] = True
# score["good_crawler"]["attack_url"] = False
# score["bad_crawler"]["attack_url"] = False
# score["regular_user"]["attack_url"] = False
# else:
# score["attacker"]["attack_url"] = False
# score["good_crawler"]["attack_url"] = False
# score["bad_crawler"]["attack_url"] = False
# score["regular_user"]["attack_url"] = False
# #--------------------- Calculate score ---------------------
# attacker_score = good_crawler_score = bad_crawler_score = regular_user_score = 0
# attacker_score = score["attacker"]["risky_http_methods"] * weights["attacker"]["risky_http_methods"]
# attacker_score = attacker_score + score["attacker"]["robots_violations"] * weights["attacker"]["robots_violations"]
# attacker_score = attacker_score + score["attacker"]["uneven_request_timing"] * weights["attacker"]["uneven_request_timing"]
# attacker_score = attacker_score + score["attacker"]["different_user_agents"] * weights["attacker"]["different_user_agents"]
# attacker_score = attacker_score + score["attacker"]["attack_url"] * weights["attacker"]["attack_url"]
# good_crawler_score = score["good_crawler"]["risky_http_methods"] * weights["good_crawler"]["risky_http_methods"]
# good_crawler_score = good_crawler_score + score["good_crawler"]["robots_violations"] * weights["good_crawler"]["robots_violations"]
# good_crawler_score = good_crawler_score + score["good_crawler"]["uneven_request_timing"] * weights["good_crawler"]["uneven_request_timing"]
# good_crawler_score = good_crawler_score + score["good_crawler"]["different_user_agents"] * weights["good_crawler"]["different_user_agents"]
# good_crawler_score = good_crawler_score + score["good_crawler"]["attack_url"] * weights["good_crawler"]["attack_url"]
# bad_crawler_score = score["bad_crawler"]["risky_http_methods"] * weights["bad_crawler"]["risky_http_methods"]
# bad_crawler_score = bad_crawler_score + score["bad_crawler"]["robots_violations"] * weights["bad_crawler"]["robots_violations"]
# bad_crawler_score = bad_crawler_score + score["bad_crawler"]["uneven_request_timing"] * weights["bad_crawler"]["uneven_request_timing"]
# bad_crawler_score = bad_crawler_score + score["bad_crawler"]["different_user_agents"] * weights["bad_crawler"]["different_user_agents"]
# bad_crawler_score = bad_crawler_score + score["bad_crawler"]["attack_url"] * weights["bad_crawler"]["attack_url"]
# regular_user_score = score["regular_user"]["risky_http_methods"] * weights["regular_user"]["risky_http_methods"]
# regular_user_score = regular_user_score + score["regular_user"]["robots_violations"] * weights["regular_user"]["robots_violations"]
# regular_user_score = regular_user_score + score["regular_user"]["uneven_request_timing"] * weights["regular_user"]["uneven_request_timing"]
# regular_user_score = regular_user_score + score["regular_user"]["different_user_agents"] * weights["regular_user"]["different_user_agents"]
# regular_user_score = regular_user_score + score["regular_user"]["attack_url"] * weights["regular_user"]["attack_url"]
# score_details = f"""
# Attacker score: {attacker_score}
# Good Crawler score: {good_crawler_score}
# Bad Crawler score: {bad_crawler_score}
# Regular User score: {regular_user_score}
# """
# app_logger.debug(score_details)
# analyzed_metrics = {"risky_http_methods": http_method_attacker_score, "robots_violations": violated_robots_ratio, "uneven_request_timing": mean, "different_user_agents": user_agents_used, "attack_url": attack_urls_found_list}
# category_scores = {"attacker": attacker_score, "good_crawler": good_crawler_score, "bad_crawler": bad_crawler_score, "regular_user": regular_user_score}
# category = max(category_scores, key=category_scores.get)
# last_analysis = datetime.now(tz=ZoneInfo('UTC'))
# self._db_manager.update_ip_stats_analysis(ip, analyzed_metrics, category, category_scores, last_analysis)
# return 0
# def update_ip_rep_infos(self, ip: str) -> list[str]:
# api_url = "https://iprep.lcrawl.com/api/iprep/"
# params = {
# "cidr": ip
# }
# headers = {
# "Content-Type": "application/json"
# }
# response = requests.get(api_url, headers=headers, params=params)
# payload = response.json()
# if payload["results"]:
# data = payload["results"][0]
# country_iso_code = data["geoip_data"]["country_iso_code"]
# asn = data["geoip_data"]["asn_autonomous_system_number"]
# asn_org = data["geoip_data"]["asn_autonomous_system_organization"]
# list_on = data["list_on"]
# sanitized_country_iso_code = sanitize_for_storage(country_iso_code, 3)
# sanitized_asn = sanitize_for_storage(asn, 100)
# sanitized_asn_org = sanitize_for_storage(asn_org, 100)
# sanitized_list_on = sanitize_dict(list_on, 100000)
# self._db_manager.update_ip_rep_infos(ip, sanitized_country_iso_code, sanitized_asn, sanitized_asn_org, sanitized_list_on)
# return

View File

@@ -1,50 +1,261 @@
#!/usr/bin/env python3
import os
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Tuple
from zoneinfo import ZoneInfo
import time
from logger import get_app_logger
import socket
import time
import requests
import yaml
@dataclass
class Config:
"""Configuration class for the deception server"""
port: int = 5000
delay: int = 100 # milliseconds
server_header: str = ""
links_length_range: Tuple[int, int] = (5, 15)
links_per_page_range: Tuple[int, int] = (10, 15)
char_space: str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
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)
server_header: str = "Apache/2.2.22 (Ubuntu)"
# Crawl limiting settings - for legitimate vs malicious crawlers
max_pages_limit: int = (
100 # Max pages limit for good crawlers and regular users (and bad crawlers/attackers if infinite_pages_for_malicious is False)
)
infinite_pages_for_malicious: bool = True # Infinite pages for malicious crawlers
ban_duration_seconds: int = 600 # Ban duration in seconds for IPs exceeding limits
# Database settings
database_path: str = "data/krawl.db"
database_retention_days: int = 30
# Analyzer settings
http_risky_methods_threshold: float = None
violated_robots_threshold: float = None
uneven_request_timing_threshold: float = None
uneven_request_timing_time_window_seconds: float = None
user_agents_used_threshold: float = None
attack_urls_threshold: float = None
_server_ip: Optional[str] = None
_server_ip_cache_time: float = 0
_ip_cache_ttl: int = 300
def get_server_ip(self, refresh: bool = False) -> Optional[str]:
"""
Get the server's own public IP address.
Excludes requests from the server itself from being tracked.
"""
current_time = time.time()
# Check if cache is valid and not forced refresh
if (
self._server_ip is not None
and not refresh
and (current_time - self._server_ip_cache_time) < self._ip_cache_ttl
):
return self._server_ip
try:
# Try multiple external IP detection services (fallback chain)
ip_detection_services = [
"https://api.ipify.org", # Plain text response
"http://ident.me", # Plain text response
"https://ifconfig.me", # Plain text response
]
ip = None
for service_url in ip_detection_services:
try:
response = requests.get(service_url, timeout=5)
if response.status_code == 200:
ip = response.text.strip()
if ip:
break
except Exception:
continue
if not ip:
get_app_logger().warning(
"Could not determine server IP from external services. "
"All IPs will be tracked (including potential server IP)."
)
return None
self._server_ip = ip
self._server_ip_cache_time = current_time
return ip
except Exception as e:
get_app_logger().warning(
f"Could not determine server IP address: {e}. "
"All IPs will be tracked (including potential server IP)."
)
return None
def refresh_server_ip(self) -> Optional[str]:
"""
Force refresh the cached server IP.
Use this if you suspect the IP has changed.
Returns:
New server IP address or None if unable to determine
"""
return self.get_server_ip(refresh=True)
@classmethod
def from_env(cls) -> 'Config':
"""Create configuration from environment variables"""
def from_yaml(cls) -> "Config":
"""Create configuration from YAML file"""
config_location = os.getenv("CONFIG_LOCATION", "config.yaml")
config_path = Path(__file__).parent.parent / config_location
try:
with open(config_path, "r") as f:
data = yaml.safe_load(f)
except FileNotFoundError:
print(
f"Error: Configuration file '{config_path}' not found.", file=sys.stderr
)
print(
f"Please create a config.yaml file or set CONFIG_LOCATION environment variable.",
file=sys.stderr,
)
sys.exit(1)
except yaml.YAMLError as e:
print(
f"Error: Invalid YAML in configuration file '{config_path}': {e}",
file=sys.stderr,
)
sys.exit(1)
if data is None:
data = {}
# Extract nested values with defaults
server = data.get("server", {})
links = data.get("links", {})
canary = data.get("canary", {})
dashboard = data.get("dashboard", {})
api = data.get("api", {})
database = data.get("database", {})
behavior = data.get("behavior", {})
analyzer = data.get("analyzer") or {}
crawl = data.get("crawl", {})
# Handle dashboard_secret_path - auto-generate if null/not set
dashboard_path = dashboard.get("secret_path")
if dashboard_path is None:
dashboard_path = f"/{os.urandom(16).hex()}"
else:
# ensure the dashboard path starts with a /
if dashboard_path[:1] != "/":
dashboard_path = f"/{dashboard_path}"
return cls(
port=int(os.getenv('PORT', 5000)),
delay=int(os.getenv('DELAY', 100)),
port=server.get("port", 5000),
delay=server.get("delay", 100),
server_header=server.get("server_header", ""),
links_length_range=(
int(os.getenv('LINKS_MIN_LENGTH', 5)),
int(os.getenv('LINKS_MAX_LENGTH', 15))
links.get("min_length", 5),
links.get("max_length", 15),
),
links_per_page_range=(
int(os.getenv('LINKS_MIN_PER_PAGE', 10)),
int(os.getenv('LINKS_MAX_PER_PAGE', 15))
links.get("min_per_page", 10),
links.get("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)),
server_header=os.getenv('SERVER_HEADER', 'Apache/2.2.22 (Ubuntu)')
char_space=links.get(
"char_space",
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
),
max_counter=links.get("max_counter", 10),
canary_token_url=canary.get("token_url"),
canary_token_tries=canary.get("token_tries", 10),
dashboard_secret_path=dashboard_path,
probability_error_codes=behavior.get("probability_error_codes", 0),
database_path=database.get("path", "data/krawl.db"),
database_retention_days=database.get("retention_days", 30),
http_risky_methods_threshold=analyzer.get(
"http_risky_methods_threshold", 0.1
),
violated_robots_threshold=analyzer.get("violated_robots_threshold", 0.1),
uneven_request_timing_threshold=analyzer.get(
"uneven_request_timing_threshold", 0.5
), # coefficient of variation
uneven_request_timing_time_window_seconds=analyzer.get(
"uneven_request_timing_time_window_seconds", 300
),
user_agents_used_threshold=analyzer.get("user_agents_used_threshold", 2),
attack_urls_threshold=analyzer.get("attack_urls_threshold", 1),
infinite_pages_for_malicious=crawl.get(
"infinite_pages_for_malicious", True
),
max_pages_limit=crawl.get("max_pages_limit", 250),
ban_duration_seconds=crawl.get("ban_duration_seconds", 600),
)
def __get_env_from_config(config: str) -> str:
env = config.upper().replace(".", "_").replace("-", "__").replace(" ", "_")
return f"KRAWL_{env}"
def override_config_from_env(config: Config = None):
"""Initialize configuration from environment variables"""
for field in config.__dataclass_fields__:
env_var = __get_env_from_config(field)
if env_var in os.environ:
get_app_logger().info(
f"Overriding config '{field}' from environment variable '{env_var}'"
)
try:
field_type = config.__dataclass_fields__[field].type
env_value = os.environ[env_var]
if field_type == int:
setattr(config, field, int(env_value))
elif field_type == float:
setattr(config, field, float(env_value))
elif field_type == bool:
# Handle boolean values (case-insensitive: true/false, yes/no, 1/0)
setattr(config, field, env_value.lower() in ("true", "yes", "1"))
elif field_type == Tuple[int, int]:
parts = env_value.split(",")
if len(parts) == 2:
setattr(config, field, (int(parts[0]), int(parts[1])))
else:
setattr(config, field, env_value)
except Exception as e:
get_app_logger().error(
f"Error overriding config '{field}' from environment variable '{env_var}': {e}"
)
_config_instance = None
def get_config() -> Config:
"""Get the singleton Config instance"""
global _config_instance
if _config_instance is None:
_config_instance = Config.from_yaml()
override_config_from_env(_config_instance)
return _config_instance

1602
src/database.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ import string
import json
from templates import html_templates
from wordlists import get_wordlists
from config import get_config
def random_username() -> str:
@@ -21,10 +22,10 @@ def random_password() -> str:
"""Generate random password"""
wl = get_wordlists()
templates = [
lambda: ''.join(random.choices(string.ascii_letters + string.digits, k=12)),
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)),
lambda: "".join(random.choices(string.ascii_lowercase, k=8)),
]
return random.choice(templates)()
@@ -37,10 +38,19 @@ def random_email(username: str = None) -> str:
return f"{username}@{random.choice(wl.email_domains)}"
def random_server_header() -> str:
"""Generate random server header from wordlists"""
config = get_config()
if config.server_header:
return config.server_header
wl = get_wordlists()
return random.choice(wl.server_headers)
def random_api_key() -> str:
"""Generate random API key"""
wl = get_wordlists()
key = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
key = "".join(random.choices(string.ascii_letters + string.digits, k=32))
return random.choice(wl.api_key_prefixes) + key
@@ -80,14 +90,16 @@ def users_json() -> str:
users = []
for i in range(random.randint(3, 8)):
username = random_username()
users.append({
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()
})
"api_token": random_api_key(),
}
)
return json.dumps({"users": users}, indent=2)
@@ -95,20 +107,28 @@ 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()
"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))
"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))
"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))
}
"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)
@@ -121,46 +141,65 @@ def api_response(path: str) -> str:
users = []
for i in range(count):
username = random_username()
users.append({
users.append(
{
"id": i + 1,
"username": username,
"email": random_email(username),
"role": random.choice(wl.user_roles)
})
"role": random.choice(wl.user_roles),
}
)
return users
responses = {
'/api/users': json.dumps({
"/api/users": json.dumps(
{
"users": random_users(random.randint(2, 5)),
"total": random.randint(50, 500)
}, indent=2),
'/api/v1/users': json.dumps({
"total": random.randint(50, 500),
},
indent=2,
),
"/api/v1/users": json.dumps(
{
"status": "success",
"data": [{
"data": [
{
"id": random.randint(1, 100),
"name": random_username(),
"api_key": random_api_key()
}]
}, indent=2),
'/api/v2/secrets': json.dumps({
"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()
"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({
"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)}
"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
@@ -172,7 +211,7 @@ 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))
@@ -184,7 +223,9 @@ def directory_listing(path: str) -> str:
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)))]
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)

113
src/geo_utils.py Normal file
View File

@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""
Geolocation utilities for reverse geocoding and city lookups.
"""
import requests
from typing import Optional, Tuple
from logger import get_app_logger
app_logger = get_app_logger()
# Simple city name cache to avoid repeated API calls
_city_cache = {}
def reverse_geocode_city(latitude: float, longitude: float) -> Optional[str]:
"""
Reverse geocode coordinates to get city name using Nominatim (OpenStreetMap).
Args:
latitude: Latitude coordinate
longitude: Longitude coordinate
Returns:
City name or None if not found
"""
# Check cache first
cache_key = f"{latitude},{longitude}"
if cache_key in _city_cache:
return _city_cache[cache_key]
try:
# Use Nominatim reverse geocoding API (free, no API key required)
url = "https://nominatim.openstreetmap.org/reverse"
params = {
"lat": latitude,
"lon": longitude,
"format": "json",
"zoom": 10, # City level
"addressdetails": 1,
}
headers = {"User-Agent": "Krawl-Honeypot/1.0"} # Required by Nominatim ToS
response = requests.get(url, params=params, headers=headers, timeout=5)
response.raise_for_status()
data = response.json()
address = data.get("address", {})
# Try to get city from various possible fields
city = (
address.get("city")
or address.get("town")
or address.get("village")
or address.get("municipality")
or address.get("county")
)
# Cache the result
_city_cache[cache_key] = city
if city:
app_logger.debug(f"Reverse geocoded {latitude},{longitude} to {city}")
return city
except requests.RequestException as e:
app_logger.warning(f"Reverse geocoding failed for {latitude},{longitude}: {e}")
return None
except Exception as e:
app_logger.error(f"Error in reverse geocoding: {e}")
return None
def get_most_recent_geoip_data(results: list) -> Optional[dict]:
"""
Extract the most recent geoip_data from API results.
Results are assumed to be sorted by record_added (most recent first).
Args:
results: List of result dictionaries from IP reputation API
Returns:
Most recent geoip_data dict or None
"""
if not results:
return None
# The first result is the most recent (sorted by record_added)
most_recent = results[0]
return most_recent.get("geoip_data")
def extract_city_from_coordinates(geoip_data: dict) -> Optional[str]:
"""
Extract city name from geoip_data using reverse geocoding.
Args:
geoip_data: Dictionary containing location_latitude and location_longitude
Returns:
City name or None
"""
if not geoip_data:
return None
latitude = geoip_data.get("location_latitude")
longitude = geoip_data.get("location_longitude")
if latitude is None or longitude is None:
return None
return reverse_geocode_city(latitude, longitude)

File diff suppressed because it is too large Load Diff

61
src/ip_utils.py Normal file
View File

@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""
IP utility functions for filtering and validating IP addresses.
Provides common IP filtering logic used across the Krawl honeypot.
"""
import ipaddress
from typing import Optional
def is_local_or_private_ip(ip_str: str) -> bool:
"""
Check if an IP address is local, private, or reserved.
Filters out:
- 127.0.0.1 (localhost)
- 127.0.0.0/8 (loopback)
- 10.0.0.0/8 (private network)
- 172.16.0.0/12 (private network)
- 192.168.0.0/16 (private network)
- 0.0.0.0/8 (this network)
- ::1 (IPv6 localhost)
- ::ffff:127.0.0.0/104 (IPv6-mapped IPv4 loopback)
Args:
ip_str: IP address string
Returns:
True if IP is local/private/reserved, False if it's public
"""
try:
ip = ipaddress.ip_address(ip_str)
return (
ip.is_private
or ip.is_loopback
or ip.is_reserved
or ip.is_link_local
or str(ip) in ("0.0.0.0", "::1")
)
except ValueError:
# Invalid IP address
return True
def is_valid_public_ip(ip: str, server_ip: Optional[str] = None) -> bool:
"""
Check if an IP is public and not the server's own IP.
Returns True only if:
- IP is not in local/private ranges AND
- IP is not the server's own public IP (if server_ip provided)
Args:
ip: IP address string to check
server_ip: Server's public IP (optional). If provided, filters out this IP too.
Returns:
True if IP is a valid public IP to track, False otherwise
"""
return not is_local_or_private_ip(ip) and (server_ip is None or ip != server_ip)

View File

@@ -8,10 +8,26 @@ Provides two loggers: app (application) and access (HTTP access logs).
import logging
import os
from logging.handlers import RotatingFileHandler
from datetime import datetime
class TimezoneFormatter(logging.Formatter):
"""Custom formatter that respects configured timezone"""
def __init__(self, fmt=None, datefmt=None):
super().__init__(fmt, datefmt)
def formatTime(self, record, datefmt=None):
"""Override formatTime to use configured timezone"""
dt = datetime.fromtimestamp(record.created)
if datefmt:
return dt.strftime(datefmt)
return dt.isoformat()
class LoggerManager:
"""Singleton logger manager for the application."""
_instance = None
def __new__(cls):
@@ -22,7 +38,7 @@ class LoggerManager:
def initialize(self, log_dir: str = "logs") -> None:
"""
Initialize the logging system with rotating file handlers.
Initialize the logging system with rotating file handlers.loggers
Args:
log_dir: Directory for log files (created if not exists)
@@ -34,9 +50,9 @@ class LoggerManager:
os.makedirs(log_dir, exist_ok=True)
# Common format for all loggers
log_format = logging.Formatter(
log_format = TimezoneFormatter(
"[%(asctime)s] %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
datefmt="%Y-%m-%d %H:%M:%S",
)
# Rotation settings: 1MB max, 5 backups
@@ -51,7 +67,7 @@ class LoggerManager:
app_file_handler = RotatingFileHandler(
os.path.join(log_dir, "krawl.log"),
maxBytes=max_bytes,
backupCount=backup_count
backupCount=backup_count,
)
app_file_handler.setFormatter(log_format)
self._app_logger.addHandler(app_file_handler)
@@ -68,7 +84,7 @@ class LoggerManager:
access_file_handler = RotatingFileHandler(
os.path.join(log_dir, "access.log"),
maxBytes=max_bytes,
backupCount=backup_count
backupCount=backup_count,
)
access_file_handler.setFormatter(log_format)
self._access_logger.addHandler(access_file_handler)
@@ -83,12 +99,12 @@ class LoggerManager:
self._credential_logger.handlers.clear()
# Credential logger uses a simple format: timestamp|ip|username|password|path
credential_format = logging.Formatter("%(message)s")
credential_format = TimezoneFormatter("%(message)s")
credential_file_handler = RotatingFileHandler(
os.path.join(log_dir, "credentials.log"),
maxBytes=max_bytes,
backupCount=backup_count
backupCount=backup_count,
)
credential_file_handler.setFormatter(credential_format)
self._credential_logger.addHandler(credential_file_handler)

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""
Migration script to add CategoryHistory table to existing databases.
Run this once to upgrade your database schema.
"""
import sys
from pathlib import Path
# Add parent directory to path to import modules
sys.path.insert(0, str(Path(__file__).parent.parent))
from database import get_database, DatabaseManager
from models import Base, CategoryHistory
def migrate():
"""Create CategoryHistory table if it doesn't exist."""
print("Starting migration: Adding CategoryHistory table...")
try:
db = get_database()
# Initialize database if not already done
if not db._initialized:
db.initialize()
# Create only the CategoryHistory table
CategoryHistory.__table__.create(db._engine, checkfirst=True)
print("✓ Migration completed successfully!")
print(" - CategoryHistory table created")
except Exception as e:
print(f"✗ Migration failed: {e}")
sys.exit(1)
if __name__ == "__main__":
migrate()

246
src/models.py Normal file
View File

@@ -0,0 +1,246 @@
#!/usr/bin/env python3
"""
SQLAlchemy ORM models for the Krawl honeypot database.
Stores access logs, credential attempts, attack detections, and IP statistics.
"""
from datetime import datetime
from typing import Optional, List, Dict
from sqlalchemy import (
String,
Integer,
Boolean,
DateTime,
Float,
ForeignKey,
Index,
JSON,
)
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from sanitizer import (
MAX_IP_LENGTH,
MAX_PATH_LENGTH,
MAX_USER_AGENT_LENGTH,
MAX_CREDENTIAL_LENGTH,
MAX_ATTACK_PATTERN_LENGTH,
MAX_CITY_LENGTH,
MAX_ASN_ORG_LENGTH,
MAX_REPUTATION_SOURCE_LENGTH,
)
class Base(DeclarativeBase):
"""Base class for all ORM models."""
pass
class AccessLog(Base):
"""
Records all HTTP requests to the honeypot.
Stores request metadata, suspicious activity flags, and timestamps
for analysis and dashboard display.
"""
__tablename__ = "access_logs"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
# ip: Mapped[str] = mapped_column(String(MAX_IP_LENGTH), nullable=False, index=True, ForeignKey('ip_logs.id', ondelete='CASCADE'))
ip: Mapped[str] = mapped_column(String(MAX_IP_LENGTH), nullable=False, index=True)
path: Mapped[str] = mapped_column(String(MAX_PATH_LENGTH), nullable=False)
user_agent: Mapped[Optional[str]] = mapped_column(
String(MAX_USER_AGENT_LENGTH), nullable=True
)
method: Mapped[str] = mapped_column(String(10), nullable=False, default="GET")
is_suspicious: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
is_honeypot_trigger: Mapped[bool] = mapped_column(
Boolean, nullable=False, default=False
)
timestamp: Mapped[datetime] = mapped_column(
DateTime, nullable=False, default=datetime.utcnow, index=True
)
# Relationship to attack detections
attack_detections: Mapped[List["AttackDetection"]] = relationship(
"AttackDetection", back_populates="access_log", cascade="all, delete-orphan"
)
# Indexes for common queries
__table_args__ = (
Index("ix_access_logs_ip_timestamp", "ip", "timestamp"),
Index("ix_access_logs_is_suspicious", "is_suspicious"),
Index("ix_access_logs_is_honeypot_trigger", "is_honeypot_trigger"),
)
def __repr__(self) -> str:
return f"<AccessLog(id={self.id}, ip='{self.ip}', path='{self.path[:50]}')>"
class CredentialAttempt(Base):
"""
Records captured login attempts from honeypot login forms.
Stores the submitted username and password along with request metadata.
"""
__tablename__ = "credential_attempts"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
ip: Mapped[str] = mapped_column(String(MAX_IP_LENGTH), nullable=False, index=True)
path: Mapped[str] = mapped_column(String(MAX_PATH_LENGTH), nullable=False)
username: Mapped[Optional[str]] = mapped_column(
String(MAX_CREDENTIAL_LENGTH), nullable=True
)
password: Mapped[Optional[str]] = mapped_column(
String(MAX_CREDENTIAL_LENGTH), nullable=True
)
timestamp: Mapped[datetime] = mapped_column(
DateTime, nullable=False, default=datetime.utcnow, index=True
)
# Composite index for common queries
__table_args__ = (Index("ix_credential_attempts_ip_timestamp", "ip", "timestamp"),)
def __repr__(self) -> str:
return f"<CredentialAttempt(id={self.id}, ip='{self.ip}', username='{self.username}')>"
class AttackDetection(Base):
"""
Records detected attack patterns in requests.
Linked to the parent AccessLog record. Multiple attack types can be
detected in a single request.
"""
__tablename__ = "attack_detections"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
access_log_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("access_logs.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
attack_type: Mapped[str] = mapped_column(String(50), nullable=False)
matched_pattern: Mapped[Optional[str]] = mapped_column(
String(MAX_ATTACK_PATTERN_LENGTH), nullable=True
)
# Relationship back to access log
access_log: Mapped["AccessLog"] = relationship(
"AccessLog", back_populates="attack_detections"
)
def __repr__(self) -> str:
return f"<AttackDetection(id={self.id}, type='{self.attack_type}')>"
class IpStats(Base):
"""
Aggregated statistics per IP address.
Includes fields for future GeoIP and reputation enrichment.
Updated on each request from an IP.
"""
__tablename__ = "ip_stats"
ip: Mapped[str] = mapped_column(String(MAX_IP_LENGTH), primary_key=True)
total_requests: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
first_seen: Mapped[datetime] = mapped_column(
DateTime, nullable=False, default=datetime.utcnow
)
last_seen: Mapped[datetime] = mapped_column(
DateTime, nullable=False, default=datetime.utcnow
)
# GeoIP fields (populated by future enrichment)
country_code: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
city: Mapped[Optional[str]] = mapped_column(String(MAX_CITY_LENGTH), nullable=True)
latitude: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
longitude: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
asn: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
asn_org: Mapped[Optional[str]] = mapped_column(
String(MAX_ASN_ORG_LENGTH), nullable=True
)
list_on: Mapped[Optional[Dict[str, str]]] = mapped_column(JSON, nullable=True)
# Reputation fields (populated by future enrichment)
reputation_score: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
reputation_source: Mapped[Optional[str]] = mapped_column(
String(MAX_REPUTATION_SOURCE_LENGTH), nullable=True
)
reputation_updated: Mapped[Optional[datetime]] = mapped_column(
DateTime, nullable=True
)
# Analyzed metrics, category and category scores
analyzed_metrics: Mapped[Dict[str, object]] = mapped_column(JSON, nullable=True)
category: Mapped[str] = mapped_column(String, nullable=True)
category_scores: Mapped[Dict[str, int]] = mapped_column(JSON, nullable=True)
manual_category: Mapped[bool] = mapped_column(Boolean, default=False, nullable=True)
last_analysis: Mapped[datetime] = mapped_column(DateTime, nullable=True)
def __repr__(self) -> str:
return f"<IpStats(ip='{self.ip}', total_requests={self.total_requests})>"
class CategoryHistory(Base):
"""
Records category changes for IP addresses over time.
Tracks when an IP's category changes, storing both the previous
and new category along with timestamp for timeline visualization.
"""
__tablename__ = "category_history"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
ip: Mapped[str] = mapped_column(String(MAX_IP_LENGTH), nullable=False, index=True)
old_category: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
new_category: Mapped[str] = mapped_column(String(50), nullable=False)
timestamp: Mapped[datetime] = mapped_column(
DateTime, nullable=False, default=datetime.utcnow, index=True
)
# Composite index for efficient IP-based timeline queries
__table_args__ = (Index("ix_category_history_ip_timestamp", "ip", "timestamp"),)
def __repr__(self) -> str:
return f"<CategoryHistory(ip='{self.ip}', {self.old_category} -> {self.new_category})>"
# class IpLog(Base):
# """
# Records all IPs that have accessed the honeypot, along with aggregated stats and inferred user category.
# """
# __tablename__ = 'ip_logs'
# id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
# ip: Mapped[str] = mapped_column(String(MAX_IP_LENGTH), nullable=False, index=True)
# stats: Mapped[List[str]] = mapped_column(String(MAX_PATH_LENGTH))
# category: Mapped[str] = mapped_column(String(15))
# manual_category: Mapped[bool] = mapped_column(Boolean, default=False)
# last_analysis: Mapped[datetime] = mapped_column(DateTime, index=True),
# # Relationship to attack detections
# access_logs: Mapped[List["AccessLog"]] = relationship(
# "AccessLog",
# back_populates="ip",
# cascade="all, delete-orphan"
# )
# # Indexes for common queries
# __table_args__ = (
# Index('ix_access_logs_ip_timestamp', 'ip', 'timestamp'),
# Index('ix_access_logs_is_suspicious', 'is_suspicious'),
# Index('ix_access_logs_is_honeypot_trigger', 'is_honeypot_trigger'),
# )
# def __repr__(self) -> str:
# return f"<AccessLog(id={self.id}, ip='{self.ip}', path='{self.path[:50]}')>"

116
src/sanitizer.py Normal file
View File

@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""
Sanitization utilities for safe database storage and HTML output.
Protects against SQL injection payloads, XSS, and storage exhaustion attacks.
"""
import html
import re
from typing import Optional, Dict
# Field length limits for database storage
MAX_IP_LENGTH = 45 # IPv6 max length
MAX_PATH_LENGTH = 2048 # URL max practical length
MAX_USER_AGENT_LENGTH = 512
MAX_CREDENTIAL_LENGTH = 256
MAX_ATTACK_PATTERN_LENGTH = 256
MAX_CITY_LENGTH = 128
MAX_ASN_ORG_LENGTH = 256
MAX_REPUTATION_SOURCE_LENGTH = 64
def sanitize_for_storage(value: Optional[str], max_length: int) -> str:
"""
Sanitize and truncate string for safe database storage.
Removes null bytes and control characters that could cause issues
with database storage or log processing.
Args:
value: The string to sanitize
max_length: Maximum length to truncate to
Returns:
Sanitized and truncated string, empty string if input is None/empty
"""
if not value:
return ""
# Convert to string if not already
value = str(value)
# Remove null bytes and control characters (except newline \n, tab \t, carriage return \r)
# Control chars are 0x00-0x1F and 0x7F, we keep 0x09 (tab), 0x0A (newline), 0x0D (carriage return)
cleaned = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]", "", value)
# Truncate to max length
return cleaned[:max_length]
def sanitize_ip(value: Optional[str]) -> str:
"""Sanitize IP address for storage."""
return sanitize_for_storage(value, MAX_IP_LENGTH)
def sanitize_path(value: Optional[str]) -> str:
"""Sanitize URL path for storage."""
return sanitize_for_storage(value, MAX_PATH_LENGTH)
def sanitize_user_agent(value: Optional[str]) -> str:
"""Sanitize user agent string for storage."""
return sanitize_for_storage(value, MAX_USER_AGENT_LENGTH)
def sanitize_credential(value: Optional[str]) -> str:
"""Sanitize username or password for storage."""
return sanitize_for_storage(value, MAX_CREDENTIAL_LENGTH)
def sanitize_attack_pattern(value: Optional[str]) -> str:
"""Sanitize matched attack pattern for storage."""
return sanitize_for_storage(value, MAX_ATTACK_PATTERN_LENGTH)
def escape_html(value: Optional[str]) -> str:
"""
Escape HTML special characters for safe display in web pages.
Prevents stored XSS attacks when displaying user-controlled data
in the dashboard.
Args:
value: The string to escape
Returns:
HTML-escaped string, empty string if input is None/empty
"""
if not value:
return ""
return html.escape(str(value))
def escape_html_truncated(value: Optional[str], max_display_length: int) -> str:
"""
Escape HTML and truncate for display.
Args:
value: The string to escape and truncate
max_display_length: Maximum display length (truncation happens before escaping)
Returns:
HTML-escaped and truncated string
"""
if not value:
return ""
value_str = str(value)
if len(value_str) > max_display_length:
value_str = value_str[:max_display_length] + "..."
return html.escape(value_str)
def sanitize_dict(value: Optional[Dict[str, str]], max_display_length):
return {k: sanitize_for_storage(v, max_display_length) for k, v in value.items()}

View File

@@ -8,51 +8,78 @@ Run this file to start the server.
import sys
from http.server import HTTPServer
from config import Config
from config import get_config
from tracker import AccessTracker
from analyzer import Analyzer
from handler import Handler
from logger import initialize_logging, get_app_logger, get_access_logger, get_credential_logger
from logger import (
initialize_logging,
get_app_logger,
get_access_logger,
get_credential_logger,
)
from database import initialize_database
from tasks_master import get_tasksmaster
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')
print(' SERVER_HEADER - HTTP Server header for deception (default: Apache/2.2.22 (Ubuntu))')
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("Configuration:")
print(" Configuration is loaded from a YAML file (default: config.yaml)")
print("Set CONFIG_LOCATION environment variable to use a different file.\n")
print("Example config.yaml structure:")
print("server:")
print("port: 5000")
print("delay: 100")
print("links:")
print("min_length: 5")
print("max_length: 15")
print("min_per_page: 10")
print("max_per_page: 15")
print("canary:")
print("token_url: null")
print("token_tries: 10")
print("dashboard:")
print("secret_path: null # auto-generated if not set")
print("database:")
print('path: "data/krawl.db"')
print("retention_days: 30")
print("behavior:")
print("probability_error_codes: 0")
def main():
"""Main entry point for the deception server"""
if '-h' in sys.argv or '--help' in sys.argv:
if "-h" in sys.argv or "--help" in sys.argv:
print_usage()
exit(0)
# Initialize logging
config = get_config()
# Initialize logging with timezone
initialize_logging()
app_logger = get_app_logger()
access_logger = get_access_logger()
credential_logger = get_credential_logger()
config = Config.from_env()
# Initialize database for persistent storage
try:
initialize_database(config.database_path)
app_logger.info(f"Database initialized at: {config.database_path}")
except Exception as e:
app_logger.warning(
f"Database initialization failed: {e}. Continuing with in-memory only."
)
tracker = AccessTracker()
tracker = AccessTracker(config.max_pages_limit, config.ban_duration_seconds)
analyzer = Analyzer()
Handler.config = config
Handler.tracker = tracker
Handler.analyzer = analyzer
Handler.counter = config.canary_token_tries
Handler.app_logger = app_logger
Handler.access_logger = access_logger
@@ -60,35 +87,55 @@ def main():
if len(sys.argv) == 2:
try:
with open(sys.argv[1], 'r') as f:
with open(sys.argv[1], "r") as f:
Handler.webpages = f.readlines()
if not Handler.webpages:
app_logger.warning('The file provided was empty. Using randomly generated links.')
app_logger.warning(
"The file provided was empty. Using randomly generated links."
)
Handler.webpages = None
except IOError:
app_logger.warning("Can't read input file. Using randomly generated links.")
try:
app_logger.info(f'Starting deception server on port {config.port}...')
app_logger.info(f'Dashboard available at: {config.dashboard_secret_path}')
if config.canary_token_url:
app_logger.info(f'Canary token will appear after {config.canary_token_tries} tries')
else:
app_logger.info('No canary token configured (set CANARY_TOKEN_URL to enable)')
# tasks master init
tasks_master = get_tasksmaster()
tasks_master.run_scheduled_tasks()
server = HTTPServer(('0.0.0.0', config.port), Handler)
app_logger.info('Server started. Use <Ctrl-C> to stop.')
try:
banner = f"""
============================================================
DASHBOARD AVAILABLE AT
{config.dashboard_secret_path}
============================================================
"""
app_logger.info(banner)
app_logger.info(f"Starting deception server on port {config.port}...")
if config.canary_token_url:
app_logger.info(
f"Canary token will appear after {config.canary_token_tries} tries"
)
else:
app_logger.info(
"No canary token configured (set CANARY_TOKEN_URL to enable)"
)
server = HTTPServer(("0.0.0.0", config.port), Handler)
app_logger.info("Server started. Use <Ctrl-C> to stop.")
server.serve_forever()
except KeyboardInterrupt:
app_logger.info('Stopping server...')
app_logger.info("Stopping server...")
server.socket.close()
app_logger.info('Server stopped')
app_logger.info("Server stopped")
except Exception as e:
app_logger.error(f'Error starting HTTP server on port {config.port}: {e}')
app_logger.error(f'Make sure you are root, if needed, and that port {config.port} is open.')
app_logger.error(f"Error starting HTTP server on port {config.port}: {e}")
app_logger.error(
f"Make sure you are root, if needed, and that port {config.port} is open."
)
exit(1)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -21,23 +21,23 @@ def generate_server_error() -> tuple[str, str]:
404: "Not Found",
500: "Internal Server Error",
502: "Bad Gateway",
503: "Service Unavailable"
503: "Service Unavailable",
}
code = random.choice(list(error_codes.keys()))
message = error_codes[code]
template = server_config.get('template', '')
version = random.choice(server_config.get('versions', ['1.0']))
template = server_config.get("template", "")
version = random.choice(server_config.get("versions", ["1.0"]))
html = template.replace('{code}', str(code))
html = html.replace('{message}', message)
html = html.replace('{version}', version)
html = template.replace("{code}", str(code))
html = html.replace("{message}", message)
html = html.replace("{version}", version)
if server_type == 'apache':
os = random.choice(server_config.get('os', ['Ubuntu']))
html = html.replace('{os}', os)
html = html.replace('{host}', 'localhost')
if server_type == "apache":
os = random.choice(server_config.get("os", ["Ubuntu"]))
html = html.replace("{os}", os)
html = html.replace("{host}", "localhost")
return (html, "text/html")
@@ -53,13 +53,13 @@ def get_server_header(server_type: str = None) -> str:
server_type = random.choice(list(server_errors.keys()))
server_config = server_errors.get(server_type, {})
version = random.choice(server_config.get('versions', ['1.0']))
version = random.choice(server_config.get("versions", ["1.0"]))
server_headers = {
'nginx': f"nginx/{version}",
'apache': f"Apache/{version}",
'iis': f"Microsoft-IIS/{version}",
'tomcat': f"Apache-Coyote/1.1"
"nginx": f"nginx/{version}",
"apache": f"Apache/{version}",
"iis": f"Microsoft-IIS/{version}",
"tomcat": f"Apache-Coyote/1.1",
}
return server_headers.get(server_type, "nginx/1.18.0")

View File

@@ -13,14 +13,14 @@ def detect_sql_injection_pattern(query_string: str) -> Optional[str]:
query_lower = query_string.lower()
patterns = {
'quote': [r"'", r'"', r'`'],
'comment': [r'--', r'#', r'/\*', r'\*/'],
'union': [r'\bunion\b', r'\bunion\s+select\b'],
'boolean': [r'\bor\b.*=.*', r'\band\b.*=.*', r"'.*or.*'.*=.*'"],
'time_based': [r'\bsleep\b', r'\bwaitfor\b', r'\bdelay\b', r'\bbenchmark\b'],
'stacked': [r';.*select', r';.*drop', r';.*insert', r';.*update', r';.*delete'],
'command': [r'\bexec\b', r'\bexecute\b', r'\bxp_cmdshell\b'],
'info_schema': [r'information_schema', r'table_schema', r'table_name'],
"quote": [r"'", r'"', r"`"],
"comment": [r"--", r"#", r"/\*", r"\*/"],
"union": [r"\bunion\b", r"\bunion\s+select\b"],
"boolean": [r"\bor\b.*=.*", r"\band\b.*=.*", r"'.*or.*'.*=.*'"],
"time_based": [r"\bsleep\b", r"\bwaitfor\b", r"\bdelay\b", r"\bbenchmark\b"],
"stacked": [r";.*select", r";.*drop", r";.*insert", r";.*update", r";.*delete"],
"command": [r"\bexec\b", r"\bexecute\b", r"\bxp_cmdshell\b"],
"info_schema": [r"information_schema", r"table_schema", r"table_name"],
}
for injection_type, pattern_list in patterns.items():
@@ -31,7 +31,9 @@ def detect_sql_injection_pattern(query_string: str) -> Optional[str]:
return None
def get_random_sql_error(db_type: str = None, injection_type: str = None) -> Tuple[str, str]:
def get_random_sql_error(
db_type: str = None, injection_type: str = None
) -> Tuple[str, str]:
wl = get_wordlists()
sql_errors = wl.sql_errors
@@ -45,8 +47,8 @@ def get_random_sql_error(db_type: str = None, injection_type: str = None) -> Tup
if injection_type and injection_type in db_errors:
errors = db_errors[injection_type]
elif 'generic' in db_errors:
errors = db_errors['generic']
elif "generic" in db_errors:
errors = db_errors["generic"]
else:
all_errors = []
for error_list in db_errors.values():
@@ -56,18 +58,20 @@ def get_random_sql_error(db_type: str = None, injection_type: str = None) -> Tup
error_message = random.choice(errors) if errors else "Database error occurred"
if '{table}' in error_message:
tables = ['users', 'products', 'orders', 'customers', 'accounts', 'sessions']
error_message = error_message.replace('{table}', random.choice(tables))
if "{table}" in error_message:
tables = ["users", "products", "orders", "customers", "accounts", "sessions"]
error_message = error_message.replace("{table}", random.choice(tables))
if '{column}' in error_message:
columns = ['id', 'name', 'email', 'password', 'username', 'created_at']
error_message = error_message.replace('{column}', random.choice(columns))
if "{column}" in error_message:
columns = ["id", "name", "email", "password", "username", "created_at"]
error_message = error_message.replace("{column}", random.choice(columns))
return (error_message, "text/plain")
def generate_sql_error_response(query_string: str, db_type: str = None) -> Tuple[str, str, int]:
def generate_sql_error_response(
query_string: str, db_type: str = None
) -> Tuple[str, str, int]:
injection_type = detect_sql_injection_pattern(query_string)
if not injection_type:
@@ -89,7 +93,7 @@ def get_sql_response_with_data(path: str, params: str) -> str:
injection_type = detect_sql_injection_pattern(params)
if injection_type in ['union', 'boolean', 'stacked']:
if injection_type in ["union", "boolean", "stacked"]:
data = {
"success": True,
"results": [
@@ -98,15 +102,14 @@ def get_sql_response_with_data(path: str, params: str) -> str:
"username": random_username(),
"email": random_email(),
"password_hash": random_password(),
"role": random.choice(["admin", "user", "moderator"])
"role": random.choice(["admin", "user", "moderator"]),
}
for i in range(1, random.randint(2, 5))
]
],
}
return json.dumps(data, indent=2)
return json.dumps({
"success": True,
"message": "Query executed successfully",
"results": []
}, indent=2)
return json.dumps(
{"success": True, "message": "Query executed successfully", "results": []},
indent=2,
)

430
src/tasks/analyze_ips.py Normal file
View File

@@ -0,0 +1,430 @@
from sqlalchemy import select
from typing import Optional
from database import get_database, DatabaseManager
from zoneinfo import ZoneInfo
from pathlib import Path
from datetime import datetime, timedelta
import re
import urllib.parse
from wordlists import get_wordlists
from config import get_config
from logger import get_app_logger
import requests
from sanitizer import sanitize_for_storage, sanitize_dict
# ----------------------
# TASK CONFIG
# ----------------------
TASK_CONFIG = {
"name": "analyze-ips",
"cron": "*/1 * * * *",
"enabled": True,
"run_when_loaded": True,
}
def main():
config = get_config()
db_manager = get_database()
app_logger = get_app_logger()
http_risky_methods_threshold = config.http_risky_methods_threshold
violated_robots_threshold = config.violated_robots_threshold
uneven_request_timing_threshold = config.uneven_request_timing_threshold
user_agents_used_threshold = config.user_agents_used_threshold
attack_urls_threshold = config.attack_urls_threshold
uneven_request_timing_time_window_seconds = (
config.uneven_request_timing_time_window_seconds
)
app_logger.debug(f"http_risky_methods_threshold: {http_risky_methods_threshold}")
score = {}
score["attacker"] = {
"risky_http_methods": False,
"robots_violations": False,
"uneven_request_timing": False,
"different_user_agents": False,
"attack_url": False,
}
score["good_crawler"] = {
"risky_http_methods": False,
"robots_violations": False,
"uneven_request_timing": False,
"different_user_agents": False,
"attack_url": False,
}
score["bad_crawler"] = {
"risky_http_methods": False,
"robots_violations": False,
"uneven_request_timing": False,
"different_user_agents": False,
"attack_url": False,
}
score["regular_user"] = {
"risky_http_methods": False,
"robots_violations": False,
"uneven_request_timing": False,
"different_user_agents": False,
"attack_url": False,
}
# 1-3 low, 4-6 mid, 7-9 high, 10-20 extreme
weights = {
"attacker": {
"risky_http_methods": 6,
"robots_violations": 4,
"uneven_request_timing": 3,
"different_user_agents": 8,
"attack_url": 15,
},
"good_crawler": {
"risky_http_methods": 1,
"robots_violations": 0,
"uneven_request_timing": 0,
"different_user_agents": 0,
"attack_url": 0,
},
"bad_crawler": {
"risky_http_methods": 2,
"robots_violations": 7,
"uneven_request_timing": 0,
"different_user_agents": 5,
"attack_url": 5,
},
"regular_user": {
"risky_http_methods": 0,
"robots_violations": 0,
"uneven_request_timing": 8,
"different_user_agents": 3,
"attack_url": 0,
},
}
# Get IPs with recent activity (last minute to match cron schedule)
recent_accesses = db_manager.get_access_logs(limit=999999999, since_minutes=1)
ips_to_analyze = {item["ip"] for item in recent_accesses}
if not ips_to_analyze:
app_logger.debug("[Background Task] analyze-ips: No recent activity, skipping")
return
for ip in ips_to_analyze:
# Get full history for this IP to perform accurate analysis
ip_accesses = db_manager.get_access_logs(limit=999999999, ip_filter=ip)
total_accesses_count = len(ip_accesses)
if total_accesses_count <= 0:
return
# Set category as "unknown" for the first 3 requests
if total_accesses_count < 3:
category = "unknown"
analyzed_metrics = {}
category_scores = {
"attacker": 0,
"good_crawler": 0,
"bad_crawler": 0,
"regular_user": 0,
"unknown": 0,
}
last_analysis = datetime.now()
db_manager.update_ip_stats_analysis(
ip, analyzed_metrics, category, category_scores, last_analysis
)
return 0
# --------------------- HTTP Methods ---------------------
get_accesses_count = len(
[item for item in ip_accesses if item["method"] == "GET"]
)
post_accesses_count = len(
[item for item in ip_accesses if item["method"] == "POST"]
)
put_accesses_count = len(
[item for item in ip_accesses if item["method"] == "PUT"]
)
delete_accesses_count = len(
[item for item in ip_accesses if item["method"] == "DELETE"]
)
head_accesses_count = len(
[item for item in ip_accesses if item["method"] == "HEAD"]
)
options_accesses_count = len(
[item for item in ip_accesses if item["method"] == "OPTIONS"]
)
patch_accesses_count = len(
[item for item in ip_accesses if item["method"] == "PATCH"]
)
if total_accesses_count > http_risky_methods_threshold:
http_method_attacker_score = (
post_accesses_count
+ put_accesses_count
+ delete_accesses_count
+ options_accesses_count
+ patch_accesses_count
) / total_accesses_count
else:
http_method_attacker_score = 0
# print(f"HTTP Method attacker score: {http_method_attacker_score}")
if http_method_attacker_score >= http_risky_methods_threshold:
score["attacker"]["risky_http_methods"] = True
score["good_crawler"]["risky_http_methods"] = False
score["bad_crawler"]["risky_http_methods"] = True
score["regular_user"]["risky_http_methods"] = False
else:
score["attacker"]["risky_http_methods"] = False
score["good_crawler"]["risky_http_methods"] = True
score["bad_crawler"]["risky_http_methods"] = False
score["regular_user"]["risky_http_methods"] = False
# --------------------- Robots Violations ---------------------
# respect robots.txt and login/config pages access frequency
robots_disallows = []
robots_path = Path(__file__).parent.parent / "templates" / "html" / "robots.txt"
with open(robots_path, "r") as f:
for line in f:
line = line.strip()
if not line:
continue
parts = line.split(":")
if parts[0] == "Disallow":
parts[1] = parts[1].rstrip("/")
# print(f"DISALLOW {parts[1]}")
robots_disallows.append(parts[1].strip())
# if 0 100% sure is good crawler, if >10% of robots violated is bad crawler or attacker
violated_robots_count = len(
[
item
for item in ip_accesses
if any(
item["path"].rstrip("/").startswith(disallow)
for disallow in robots_disallows
)
]
)
# print(f"Violated robots count: {violated_robots_count}")
if total_accesses_count > 0:
violated_robots_ratio = violated_robots_count / total_accesses_count
else:
violated_robots_ratio = 0
if violated_robots_ratio >= violated_robots_threshold:
score["attacker"]["robots_violations"] = True
score["good_crawler"]["robots_violations"] = False
score["bad_crawler"]["robots_violations"] = True
score["regular_user"]["robots_violations"] = False
else:
score["attacker"]["robots_violations"] = False
score["good_crawler"]["robots_violations"] = False
score["bad_crawler"]["robots_violations"] = False
score["regular_user"]["robots_violations"] = False
# --------------------- Requests Timing ---------------------
# Request rate and timing: steady, throttled, polite vs attackers' bursty, aggressive, or oddly rhythmic behavior
timestamps = [datetime.fromisoformat(item["timestamp"]) for item in ip_accesses]
now_utc = datetime.now()
timestamps = [
ts
for ts in timestamps
if now_utc - ts
<= timedelta(seconds=uneven_request_timing_time_window_seconds)
]
timestamps = sorted(timestamps, reverse=True)
time_diffs = []
for i in range(0, len(timestamps) - 1):
diff = (timestamps[i] - timestamps[i + 1]).total_seconds()
time_diffs.append(diff)
mean = 0
variance = 0
std = 0
cv = 0
if time_diffs:
mean = sum(time_diffs) / len(time_diffs)
variance = sum((x - mean) ** 2 for x in time_diffs) / len(time_diffs)
std = variance**0.5
cv = std / mean
app_logger.debug(
f"Mean: {mean} - Variance {variance} - Standard Deviation {std} - Coefficient of Variation: {cv}"
)
if cv >= uneven_request_timing_threshold:
score["attacker"]["uneven_request_timing"] = True
score["good_crawler"]["uneven_request_timing"] = False
score["bad_crawler"]["uneven_request_timing"] = False
score["regular_user"]["uneven_request_timing"] = True
else:
score["attacker"]["uneven_request_timing"] = False
score["good_crawler"]["uneven_request_timing"] = False
score["bad_crawler"]["uneven_request_timing"] = False
score["regular_user"]["uneven_request_timing"] = False
# --------------------- Different User Agents ---------------------
# Header Quality and Consistency: Crawlers tend to use complete and consistent headers, attackers might miss, fake, or change headers
user_agents_used = [item["user_agent"] for item in ip_accesses]
user_agents_used = list(dict.fromkeys(user_agents_used))
# print(f"User agents used: {user_agents_used}")
if len(user_agents_used) >= user_agents_used_threshold:
score["attacker"]["different_user_agents"] = True
score["good_crawler"]["different_user_agents"] = False
score["bad_crawler"]["different_user_agentss"] = True
score["regular_user"]["different_user_agents"] = False
else:
score["attacker"]["different_user_agents"] = False
score["good_crawler"]["different_user_agents"] = False
score["bad_crawler"]["different_user_agents"] = False
score["regular_user"]["different_user_agents"] = False
# --------------------- Attack URLs ---------------------
attack_urls_found_list = []
wl = get_wordlists()
if wl.attack_patterns:
queried_paths = [item["path"] for item in ip_accesses]
for queried_path in queried_paths:
# URL decode the path to catch encoded attacks
try:
decoded_path = urllib.parse.unquote(queried_path)
# Double decode to catch double-encoded attacks
decoded_path_twice = urllib.parse.unquote(decoded_path)
except Exception:
decoded_path = queried_path
decoded_path_twice = queried_path
for name, pattern in wl.attack_patterns.items():
# Check original, decoded, and double-decoded paths
if (
re.search(pattern, queried_path, re.IGNORECASE)
or re.search(pattern, decoded_path, re.IGNORECASE)
or re.search(pattern, decoded_path_twice, re.IGNORECASE)
):
attack_urls_found_list.append(f"{name}: {pattern}")
# remove duplicates
attack_urls_found_list = set(attack_urls_found_list)
attack_urls_found_list = list(attack_urls_found_list)
if len(attack_urls_found_list) >= attack_urls_threshold:
score["attacker"]["attack_url"] = True
score["good_crawler"]["attack_url"] = False
score["bad_crawler"]["attack_url"] = False
score["regular_user"]["attack_url"] = False
else:
score["attacker"]["attack_url"] = False
score["good_crawler"]["attack_url"] = False
score["bad_crawler"]["attack_url"] = False
score["regular_user"]["attack_url"] = False
# --------------------- Calculate score ---------------------
attacker_score = good_crawler_score = bad_crawler_score = regular_user_score = 0
attacker_score = (
score["attacker"]["risky_http_methods"]
* weights["attacker"]["risky_http_methods"]
)
attacker_score = (
attacker_score
+ score["attacker"]["robots_violations"]
* weights["attacker"]["robots_violations"]
)
attacker_score = (
attacker_score
+ score["attacker"]["uneven_request_timing"]
* weights["attacker"]["uneven_request_timing"]
)
attacker_score = (
attacker_score
+ score["attacker"]["different_user_agents"]
* weights["attacker"]["different_user_agents"]
)
attacker_score = (
attacker_score
+ score["attacker"]["attack_url"] * weights["attacker"]["attack_url"]
)
good_crawler_score = (
score["good_crawler"]["risky_http_methods"]
* weights["good_crawler"]["risky_http_methods"]
)
good_crawler_score = (
good_crawler_score
+ score["good_crawler"]["robots_violations"]
* weights["good_crawler"]["robots_violations"]
)
good_crawler_score = (
good_crawler_score
+ score["good_crawler"]["uneven_request_timing"]
* weights["good_crawler"]["uneven_request_timing"]
)
good_crawler_score = (
good_crawler_score
+ score["good_crawler"]["different_user_agents"]
* weights["good_crawler"]["different_user_agents"]
)
good_crawler_score = (
good_crawler_score
+ score["good_crawler"]["attack_url"]
* weights["good_crawler"]["attack_url"]
)
bad_crawler_score = (
score["bad_crawler"]["risky_http_methods"]
* weights["bad_crawler"]["risky_http_methods"]
)
bad_crawler_score = (
bad_crawler_score
+ score["bad_crawler"]["robots_violations"]
* weights["bad_crawler"]["robots_violations"]
)
bad_crawler_score = (
bad_crawler_score
+ score["bad_crawler"]["uneven_request_timing"]
* weights["bad_crawler"]["uneven_request_timing"]
)
bad_crawler_score = (
bad_crawler_score
+ score["bad_crawler"]["different_user_agents"]
* weights["bad_crawler"]["different_user_agents"]
)
bad_crawler_score = (
bad_crawler_score
+ score["bad_crawler"]["attack_url"] * weights["bad_crawler"]["attack_url"]
)
regular_user_score = (
score["regular_user"]["risky_http_methods"]
* weights["regular_user"]["risky_http_methods"]
)
regular_user_score = (
regular_user_score
+ score["regular_user"]["robots_violations"]
* weights["regular_user"]["robots_violations"]
)
regular_user_score = (
regular_user_score
+ score["regular_user"]["uneven_request_timing"]
* weights["regular_user"]["uneven_request_timing"]
)
regular_user_score = (
regular_user_score
+ score["regular_user"]["different_user_agents"]
* weights["regular_user"]["different_user_agents"]
)
regular_user_score = (
regular_user_score
+ score["regular_user"]["attack_url"]
* weights["regular_user"]["attack_url"]
)
score_details = f"""
Attacker score: {attacker_score}
Good Crawler score: {good_crawler_score}
Bad Crawler score: {bad_crawler_score}
Regular User score: {regular_user_score}
"""
app_logger.debug(score_details)
analyzed_metrics = {
"risky_http_methods": http_method_attacker_score,
"robots_violations": violated_robots_ratio,
"uneven_request_timing": mean,
"different_user_agents": user_agents_used,
"attack_url": attack_urls_found_list,
}
category_scores = {
"attacker": attacker_score,
"good_crawler": good_crawler_score,
"bad_crawler": bad_crawler_score,
"regular_user": regular_user_score,
}
category = max(category_scores, key=category_scores.get)
last_analysis = datetime.now()
db_manager.update_ip_stats_analysis(
ip, analyzed_metrics, category, category_scores, last_analysis
)
return

73
src/tasks/fetch_ip_rep.py Normal file
View File

@@ -0,0 +1,73 @@
from database import get_database
from logger import get_app_logger
import requests
from sanitizer import sanitize_for_storage, sanitize_dict
from geo_utils import get_most_recent_geoip_data, extract_city_from_coordinates
# ----------------------
# TASK CONFIG
# ----------------------
TASK_CONFIG = {
"name": "fetch-ip-rep",
"cron": "*/5 * * * *",
"enabled": True,
"run_when_loaded": True,
}
def main():
db_manager = get_database()
app_logger = get_app_logger()
# Only get IPs that haven't been enriched yet
unenriched_ips = db_manager.get_unenriched_ips(limit=50)
app_logger.info(
f"{len(unenriched_ips)} IP's need to be have reputation enrichment."
)
for ip in unenriched_ips:
try:
api_url = "https://iprep.lcrawl.com/api/iprep/"
params = {"cidr": ip}
headers = {"Content-Type": "application/json"}
response = requests.get(api_url, headers=headers, params=params, timeout=10)
payload = response.json()
if payload.get("results"):
results = payload["results"]
# Get the most recent result (first in list, sorted by record_added)
most_recent = results[0]
geoip_data = most_recent.get("geoip_data", {})
list_on = most_recent.get("list_on", {})
# Extract standard fields
country_iso_code = geoip_data.get("country_iso_code")
asn = geoip_data.get("asn_autonomous_system_number")
asn_org = geoip_data.get("asn_autonomous_system_organization")
latitude = geoip_data.get("location_latitude")
longitude = geoip_data.get("location_longitude")
# Extract city from coordinates using reverse geocoding
city = extract_city_from_coordinates(geoip_data)
sanitized_country_iso_code = sanitize_for_storage(country_iso_code, 3)
sanitized_asn = sanitize_for_storage(asn, 100)
sanitized_asn_org = sanitize_for_storage(asn_org, 100)
sanitized_city = sanitize_for_storage(city, 100) if city else None
sanitized_list_on = sanitize_dict(list_on, 100000)
db_manager.update_ip_rep_infos(
ip,
sanitized_country_iso_code,
sanitized_asn,
sanitized_asn_org,
sanitized_list_on,
sanitized_city,
latitude,
longitude,
)
except requests.RequestException as e:
app_logger.warning(f"Failed to fetch IP rep for {ip}: {e}")
except Exception as e:
app_logger.error(f"Error processing IP {ip}: {e}")

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""
Memory cleanup task for Krawl honeypot.
Periodically trims unbounded in-memory structures to prevent OOM.
"""
from database import get_database
from logger import get_app_logger
# ----------------------
# TASK CONFIG
# ----------------------
TASK_CONFIG = {
"name": "memory-cleanup",
"cron": "*/5 * * * *", # Run every 5 minutes
"enabled": True,
"run_when_loaded": False,
}
app_logger = get_app_logger()
def main():
"""
Clean up in-memory structures in the tracker.
Called periodically to prevent unbounded memory growth.
"""
try:
# Import here to avoid circular imports
from handler import Handler
if not Handler.tracker:
app_logger.warning("Tracker not initialized, skipping memory cleanup")
return
# Get memory stats before cleanup
stats_before = Handler.tracker.get_memory_stats()
# Run cleanup
Handler.tracker.cleanup_memory()
# Get memory stats after cleanup
stats_after = Handler.tracker.get_memory_stats()
# Log changes
access_log_reduced = (
stats_before["access_log_size"] - stats_after["access_log_size"]
)
cred_reduced = (
stats_before["credential_attempts_size"]
- stats_after["credential_attempts_size"]
)
if access_log_reduced > 0 or cred_reduced > 0:
app_logger.info(
f"Memory cleanup: Trimmed {access_log_reduced} access logs, "
f"{cred_reduced} credential attempts"
)
# Log current memory state for monitoring
app_logger.debug(
f"Memory stats after cleanup: "
f"access_logs={stats_after['access_log_size']}, "
f"credentials={stats_after['credential_attempts_size']}, "
f"unique_ips={stats_after['unique_ips_tracked']}"
)
except Exception as e:
app_logger.error(f"Error during memory cleanup: {e}")

View File

@@ -0,0 +1,76 @@
# tasks/export_malicious_ips.py
import os
from logger import get_app_logger
from database import get_database
from config import get_config
from models import IpStats
from ip_utils import is_valid_public_ip
app_logger = get_app_logger()
# ----------------------
# TASK CONFIG
# ----------------------
TASK_CONFIG = {
"name": "export-malicious-ips",
"cron": "*/5 * * * *",
"enabled": True,
"run_when_loaded": True,
}
EXPORTS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "exports")
OUTPUT_FILE = os.path.join(EXPORTS_DIR, "malicious_ips.txt")
# ----------------------
# TASK LOGIC
# ----------------------
def main():
"""
Export all attacker IPs to a text file, matching the "Attackers by Total Requests" dashboard table.
Uses the same query as the dashboard: IpStats where category == "attacker", ordered by total_requests.
TasksMaster will call this function based on the cron schedule.
"""
task_name = TASK_CONFIG.get("name")
app_logger.info(f"[Background Task] {task_name} starting...")
try:
db = get_database()
session = db.session
# Query attacker IPs from IpStats (same as dashboard "Attackers by Total Requests")
attackers = (
session.query(IpStats)
.filter(IpStats.category == "attacker")
.order_by(IpStats.total_requests.desc())
.all()
)
# Filter out local/private IPs and the server's own IP
config = get_config()
server_ip = config.get_server_ip()
public_ips = [
attacker.ip
for attacker in attackers
if is_valid_public_ip(attacker.ip, server_ip)
]
# Ensure exports directory exists
os.makedirs(EXPORTS_DIR, exist_ok=True)
# Write IPs to file (one per line)
with open(OUTPUT_FILE, "w") as f:
for ip in public_ips:
f.write(f"{ip}\n")
app_logger.info(
f"[Background Task] {task_name} exported {len(public_ips)} attacker IPs "
f"(filtered {len(attackers) - len(public_ips)} local/private IPs) to {OUTPUT_FILE}"
)
except Exception as e:
app_logger.error(f"[Background Task] {task_name} failed: {e}")
finally:
db.close_session()

321
src/tasks_master.py Normal file
View File

@@ -0,0 +1,321 @@
import os
import sys
import datetime
import functools
import threading
import importlib
import importlib.util
from logger import (
initialize_logging,
get_app_logger,
get_access_logger,
get_credential_logger,
)
app_logger = get_app_logger()
try:
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR
except ModuleNotFoundError:
msg = (
"Required modules are not installed. "
"Can not continue with module / application loading.\n"
"Install it with: pip install -r requirements"
)
print(msg, file=sys.stderr)
app_logger.error(msg)
exit()
# ---------- TASKSMASTER CLASS ----------
class TasksMaster:
TASK_DEFAULT_CRON = "*/15 * * * *"
TASK_JITTER = 240
TASKS_FOLDER = os.path.join(os.path.dirname(__file__), "tasks")
def __init__(self, scheduler: BackgroundScheduler):
self.tasks = self._config_tasks()
self.scheduler = scheduler
self.last_run_times = {}
self.scheduler.add_listener(
self.job_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR
)
def _config_tasks(self):
"""
Loads tasks from the TASKS_FOLDER and logs how many were found.
"""
tasks_defined = self._load_tasks_from_folder(self.TASKS_FOLDER)
app_logger.info(f"Scheduled Tasks Loaded from folder: {self.TASKS_FOLDER}")
return tasks_defined
def _load_tasks_from_folder(self, folder_path):
"""
Loads and registers task modules from a specified folder.
This function scans the given folder for Python (.py) files, dynamically
imports each as a module, and looks for two attributes:
- TASK_CONFIG: A dictionary containing task metadata, specifically the
'name' and 'cron' (cron schedule string).
- main: A callable function that represents the task's execution logic.
Tasks with both attributes are added to a list with their configuration and
execution function.
Args:
folder_path (str): Path to the folder containing task scripts.
Returns:
list[dict]: A list of task definitions with keys:
- 'name' (str): The name of the task.
- 'filename' (str): The file the task was loaded from.
- 'cron' (str): The crontab string for scheduling.
- 'enabled' (bool): Whether the task is enabled.
- 'run_when_loaded' (bool): Whether to run the task immediately.
"""
tasks = []
if not os.path.exists(folder_path):
app_logger.error(f"{folder_path} does not exist! Unable to load tasks!")
return tasks
# we sort the files so that we have a set order, which helps with debugging
for filename in sorted(os.listdir(folder_path)):
# skip any non python files, as well as any __pycache__ or .pyc files that might creep in there
if not filename.endswith(".py") or filename.startswith("__"):
continue
path = os.path.join(folder_path, filename)
module_name = filename[:-3]
spec = importlib.util.spec_from_file_location(f"tasks.{module_name}", path)
module = importlib.util.module_from_spec(spec)
try:
spec.loader.exec_module(module)
sys.modules[f"tasks.{module_name}"] = module
except Exception as e:
app_logger.error(f"Failed to import {filename}: {e}")
continue
# if we have a tasks config and a main function, we attempt to schedule it
if hasattr(module, "TASK_CONFIG") and hasattr(module, "main"):
# ensure task_config is a dict
if not isinstance(module.TASK_CONFIG, dict):
app_logger.error(
f"TASK_CONFIG is not a dict in {filename}. Skipping task."
)
continue
task_cron = module.TASK_CONFIG.get("cron") or self.TASK_DEFAULT_CRON
task_name = module.TASK_CONFIG.get("name", module_name)
# ensure the task_cron is a valid cron value
try:
CronTrigger.from_crontab(task_cron)
except ValueError as ve:
app_logger.error(
f"Invalid cron format for task {task_name}: {ve} - Skipping this task"
)
continue
task = {
"name": module.TASK_CONFIG.get("name", module_name),
"filename": filename,
"cron": task_cron,
"enabled": module.TASK_CONFIG.get("enabled", False),
"run_when_loaded": module.TASK_CONFIG.get("run_when_loaded", False),
}
tasks.append(task)
# we are missing things, and we log what's missing
else:
if not hasattr(module, "TASK_CONFIG"):
app_logger.warning(f"Missing TASK_CONFIG in {filename}")
elif not hasattr(module, "main"):
app_logger.warning(f"Missing main() in {filename}")
return tasks
def _add_jobs(self):
# for each task in the tasks config file...
for task_to_run in self.tasks:
# remember, these tasks, are built from the "load_tasks_from_folder" function,
# if you want to pass data from the TASKS_CONFIG dict, you need to pass it there to get it here.
task_name = task_to_run.get("name")
run_when_loaded = task_to_run.get("run_when_loaded")
module_name = os.path.splitext(task_to_run.get("filename"))[0]
task_enabled = task_to_run.get("enabled", False)
# if no crontab set for this task, we use 15 as the default.
task_cron = task_to_run.get("cron") or self.TASK_DEFAULT_CRON
# if task is disabled, skip this one
if not task_enabled:
app_logger.info(
f"{task_name} is disabled in client config. Skipping task"
)
continue
try:
if os.path.isfile(
os.path.join(self.TASKS_FOLDER, task_to_run.get("filename"))
):
# schedule the task now that everything has checked out above...
self._schedule_task(
task_name, module_name, task_cron, run_when_loaded
)
app_logger.info(
f"Scheduled {module_name} cron is set to {task_cron}.",
extra={"task": task_to_run},
)
else:
app_logger.info(
f"Skipping invalid or unsafe file: {task_to_run.get('filename')}",
extra={"task": task_to_run},
)
except Exception as e:
app_logger.error(
f"Error scheduling task: {e}", extra={"tasks": task_to_run}
)
def _schedule_task(self, task_name, module_name, task_cron, run_when_loaded):
try:
# Dynamically import the module
module = importlib.import_module(f"tasks.{module_name}")
# Check if the module has a 'main' function
if hasattr(module, "main"):
app_logger.info(f"Scheduling {task_name} - {module_name} Main Function")
# unique_job_id
job_identifier = f"{module_name}__{task_name}"
# little insurance to make sure the cron is set to something and not none
if task_cron is None:
task_cron = self.TASK_DEFAULT_CRON
trigger = CronTrigger.from_crontab(task_cron)
# schedule the task / job
if run_when_loaded:
app_logger.info(
f"Task: {task_name} is set to run instantly. Scheduling to run on scheduler start"
)
self.scheduler.add_job(
module.main,
trigger,
id=job_identifier,
jitter=self.TASK_JITTER,
name=task_name,
next_run_time=datetime.datetime.now(),
max_instances=1,
)
else:
self.scheduler.add_job(
module.main,
trigger,
id=job_identifier,
jitter=self.TASK_JITTER,
name=task_name,
max_instances=1,
)
else:
app_logger.error(f"{module_name} does not define a 'main' function.")
except Exception as e:
app_logger.error(f"Failed to load {module_name}: {e}")
def job_listener(self, event):
job_id = event.job_id
self.last_run_times[job_id] = datetime.datetime.now()
if event.exception:
app_logger.error(f"Job {event.job_id} failed: {event.exception}")
else:
app_logger.info(f"Job {event.job_id} completed successfully.")
def list_jobs(self):
scheduled_jobs = self.scheduler.get_jobs()
jobs_list = []
for job in scheduled_jobs:
jobs_list.append(
{
"id": job.id,
"name": job.name,
"next_run": job.next_run_time,
}
)
return jobs_list
def run_scheduled_tasks(self):
"""
Runs and schedules enabled tasks using the background scheduler.
This method performs the following:
1. Retrieves the current task configurations and updates internal state.
2. Adds new jobs to the scheduler based on the latest configuration.
3. Starts the scheduler to begin executing tasks at their defined intervals.
This ensures the scheduler is always running with the most up-to-date
task definitions and enabled status.
"""
# Add enabled tasks to the scheduler
self._add_jobs()
# Start the scheduler to begin executing the scheduled tasks (if not already running)
if not self.scheduler.running:
self.scheduler.start()
# ---------- SINGLETON WRAPPER ----------
T = type
def singleton_loader(func):
"""Decorator to ensure only one instance exists."""
cache: dict[str, T] = {}
lock = threading.Lock()
@functools.wraps(func)
def wrapper(*args, **kwargs) -> T:
with lock:
if func.__name__ not in cache:
cache[func.__name__] = func(*args, **kwargs)
return cache[func.__name__]
return wrapper
@singleton_loader
def get_tasksmaster(scheduler: BackgroundScheduler | None = None) -> TasksMaster:
"""
Returns the singleton TasksMaster instance.
- Automatically creates a BackgroundScheduler if none is provided.
- Automatically starts the scheduler when the singleton is created.
:param scheduler: Optional APScheduler instance. If None, a new BackgroundScheduler will be created.
"""
if scheduler is None:
scheduler = BackgroundScheduler()
tm_instance = TasksMaster(scheduler)
# Auto-start scheduler if not already running
if not scheduler.running:
scheduler.start()
app_logger.info(
"TasksMaster scheduler started automatically with singleton creation."
)
return tm_instance

View File

@@ -8,8 +8,8 @@ from .template_loader import load_template, clear_cache, TemplateNotFoundError
from . import html_templates
__all__ = [
'load_template',
'clear_cache',
'TemplateNotFoundError',
'html_templates',
"load_template",
"clear_cache",
"TemplateNotFoundError",
"html_templates",
]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,106 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Krawl me!</title>
<style>
body {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #0d1117;
color: #c9d1d9;
margin: 0;
padding: 0;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
}}
.container {{
max-width: 1200px;
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
padding: 20px;
box-sizing: border-box;
}}
h1 {{
color: #f85149;
text-align: center;
font-size: 36px;
margin: 40px 0 20px 0;
flex-shrink: 0;
}}
.counter {{
color: #f85149;
text-align: center;
font-size: 32px;
font-weight: bold;
margin: 0 0 30px 0;
flex-shrink: 0;
}}
.links-container {{
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
overflow-y: auto;
overflow-x: hidden;
flex: 1;
padding-top: 10px;
}}
.links-container::-webkit-scrollbar {{
width: 0px;
}}
.link-box {{
background: #161b22;
border: 1px solid #30363d;
border-radius: 6px;
padding: 10px 20px;
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: 16px;
font-weight: 700;
}}
a:hover {{
color: #79c0ff;
}}
.canary-token {{
background: #1c1917;
border: 2px solid #f85149;
border-radius: 8px;
padding: 20px 30px;
margin: 20px auto;
max-width: 800px;
overflow-x: auto;
}}
.canary-token a {{
color: #f85149;
font-size: 14px;
white-space: nowrap;
}}
</style>
</head>
<body>
<div class="container">
<h1>Krawl me!</h1>
<div class="counter">{counter}</div>
<div class="links-container">
{content}
</div>
</div>
</body>
</html>

View File

@@ -60,3 +60,8 @@ def product_search() -> str:
def input_form() -> str:
"""Generate input form page for XSS honeypot"""
return load_template("input_form")
def main_page(counter: int, content: str) -> str:
"""Generate main Krawl page with links and canary token"""
return load_template("main_page", counter=counter, content=content)

View File

@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer1"><g
id="g21250"
transform="matrix(0.9765625,0,0,0.9765625,1536.0434,1186.1434)"
style="display:inline"><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1241.1385,-1007.2559 c -0.6853,-0.9666 -1.7404,-3.1071 -1.7404,-3.5311 0,-0.2316 -0.3925,-0.9705 -0.8724,-1.6421 -0.4797,-0.6717 -1.1665,-1.8179 -1.5259,-2.5474 -0.9428,-1.9133 -0.8327,-2.4052 1.0817,-4.8313 2.0393,-2.5844 5.4954,-7.751 7.5001,-11.212 6.6836,-11.5394 10.2543,-26.3502 10.2918,-42.6902 0.014,-6.1916 -0.3138,-11.1512 -1.4222,-21.504 -0.2511,-2.3446 -0.6286,-6.0107 -0.8388,-8.1469 -0.2102,-2.1362 -0.4642,-4.5234 -0.5643,-5.3051 -0.1004,-0.7815 -0.2787,-2.4013 -0.3968,-3.5996 -0.1181,-1.1984 -0.3302,-2.6905 -0.4713,-3.3156 -0.1411,-0.6253 -0.3476,-1.9042 -0.4588,-2.842 -0.5672,-4.7787 -3.2292,-17.1285 -4.7783,-22.1672 -0.4165,-1.3546 -1.1796,-3.9124 -1.6957,-5.6838 -0.5161,-1.7715 -1.6975,-5.4802 -2.6255,-8.2417 -4.6459,-13.8253 -4.9757,-16.427 -2.6904,-21.2198 2.0776,-4.3574 6.2598,-6.6975 11.403,-6.3802 1.8507,0.1141 3.6912,0.539 8.9047,2.0557 1.6153,0.47 3.4482,0.9897 4.0735,1.155 0.6252,0.1653 2.373,0.7217 3.884,1.2364 4.9437,1.6843 6.8819,2.3162 9.189,2.9957 1.2504,0.3683 2.6145,0.8262 3.0313,1.0174 1.1713,0.5374 2.7637,1.1747 3.5998,1.4405 1.4598,0.4641 5.4471,1.9658 6.6964,2.522 4.255,1.8943 7.767,3.4118 8.1765,3.5329 0.2605,0.077 1.9656,0.8866 3.7893,1.7989 1.8235,0.9123 4.2107,2.0926 5.3049,2.6231 1.0942,0.5304 2.6714,1.3307 3.5051,1.7785 0.8335,0.4478 2.4535,1.3177 3.5997,1.9331 2.5082,1.3467 8.2672,4.7786 10.5669,6.2972 0.9141,0.6037 2.589,1.6943 3.7218,2.4238 1.1329,0.7294 2.6443,1.763 3.3586,2.2968 0.7145,0.5337 1.6835,1.2158 2.1534,1.5157 0.4699,0.2998 2.1752,1.5683 3.7895,2.8188 1.6144,1.2504 3.4399,2.6571 4.0566,3.126 1.8302,1.3913 7.6176,6.4077 9.962,8.6346 1.1986,1.1386 2.4349,2.2909 2.7472,2.5607 0.9207,0.7952 9.8749,9.9437 11.9472,12.2064 3.2265,3.523 6.8834,8.0165 12.5068,15.3683 4.6009,6.0149 5.4863,7.2209 8.1198,11.0588 0.6078,0.8857 1.4643,2.0367 1.9035,2.5577 1.8373,2.1799 1.7315,3.9414 -0.2526,4.2075 -0.7601,0.1024 -0.7601,0.1024 -5.9354,-4.9924 -7.7501,-7.6289 -16.7228,-15.5916 -23.3473,-20.7192 -0.6058,-0.4689 -1.6709,-1.3213 -2.3668,-1.8946 -1.1741,-0.9668 -2.9131,-2.2747 -7.9753,-5.9975 -3.3158,-2.4387 -15.7898,-10.6751 -16.1672,-10.6751 -0.046,0 -0.9668,-0.5405 -2.0468,-1.2011 -1.0801,-0.6606 -3.0295,-1.7804 -4.332,-2.4886 -1.3026,-0.7081 -3.3488,-1.8207 -4.5472,-2.4723 -9.458,-5.1431 -18.9529,-9.5468 -26.1458,-12.1266 -11.9189,-4.2748 -14.3961,-5.0584 -21.4093,-6.7727 -8.4966,-2.0771 -8.9929,-2.1657 -9.9263,-1.7716 -0.8527,0.3599 -0.8888,1.4351 -0.1228,3.6579 0.3803,1.1037 0.5808,1.9703 1.4384,6.218 0.7976,3.9505 1.8022,9.4376 2.1677,11.8414 0.087,0.5732 0.3282,2.0226 0.5356,3.2209 0.573,3.3125 1.3897,9.8038 1.74,13.8308 0.1132,1.3025 0.415,4.5424 0.6706,7.1996 1.2443,12.9373 1.4786,18.1876 1.3605,30.5035 -0.106,11.0649 -0.2174,12.4773 -1.9191,24.346 -1.0104,7.0472 -2.8029,14.646 -5.1398,21.7882 -2.6396,8.0677 -7.4463,15.7878 -11.7695,18.9032 -0.4008,0.2889 -1.3683,0.9881 -2.1498,1.554 -2.3051,1.669 -5.9083,3.3112 -8.7153,3.9722 -1.7095,0.4024 -2.0017,0.3753 -2.4278,-0.2255 z"
id="path21283" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1344.6204,-830.62232 c -6.8773,-2.01541 -12.9376,-5.17715 -21.9342,-11.44321 -11.9734,-8.33945 -21.8594,-22.80374 -21.9023,-32.04531 -0.025,-5.21916 1.4471,-8.79863 5.9642,-14.50954 0.6662,-0.84223 1.8506,-2.36869 2.6322,-3.39215 0.7815,-1.02347 1.6434,-2.12479 1.9151,-2.4474 0.2719,-0.32261 1.168,-1.48177 1.9914,-2.57591 0.8234,-1.09416 3.8768,-4.97341 6.785,-8.62057 2.9084,-3.64716 5.5592,-6.97223 5.8908,-7.38905 1.3392,-1.68346 1.3506,-1.83796 0.207,-2.81492 -5.4037,-4.61652 -13.9573,-19.03987 -17.2069,-29.01484 -0.2037,-0.62524 -0.6723,-1.94674 -1.0413,-2.93668 -0.7402,-1.98575 -1.8645,-5.71704 -2.255,-7.48379 -1.8287,-8.27417 -2.1744,-22.61767 -0.7283,-30.21933 0.1487,-0.78153 0.3973,-2.33949 0.5523,-3.46211 0.4319,-3.12594 1.2016,-5.62552 4.5929,-14.91587 0.7521,-2.06 4.7855,-9.6636 5.9297,-11.1782 2.1853,-2.8926 2.2231,-3.2679 0.5445,-5.3997 -7.4283,-9.4333 -13.6635,-24.3793 -15.2216,-36.4873 -1.3218,-10.271 -1.1235,-23.1421 0.4668,-30.2984 0.9613,-4.3261 1.3428,-5.5729 3.7393,-12.2204 1.3168,-3.6525 4.53,-10.2639 7.0297,-14.4641 0.6414,-1.0779 1.1662,-2.0025 1.1662,-2.0549 0,-0.1073 1.4953,-2.2836 3.0347,-4.4166 6.9984,-9.6974 16.482,-18.5941 25.7084,-24.1172 2.879,-1.7236 4.055,-2.4075 4.1393,-2.4075 0.051,0 0.4349,-0.2167 0.8544,-0.4815 0.4195,-0.2649 1.4623,-0.7866 2.3172,-1.1594 0.8549,-0.3727 1.8954,-0.829 2.3122,-1.014 1.1008,-0.4884 5.5833,-2.148 7.6664,-2.8386 2.3895,-0.7922 6.1267,-1.6365 8.3432,-1.885 0.99,-0.111 2.6526,-0.298 3.6946,-0.4155 3.3891,-0.3824 11.9886,0.011 15.1571,0.6944 0.7293,0.1571 2.4345,0.4601 3.7892,0.6733 4.9466,0.7783 13.676,3.9822 18.7546,6.8835 0.939,0.5364 2.1173,1.1859 2.6184,1.4432 0.5011,0.2573 1.4816,0.9244 2.1789,1.4823 0.6972,0.558 1.6066,1.2319 2.0208,1.4976 8.9372,5.7333 22.8368,21.4683 26.7195,30.2479 0.2352,0.5317 0.9909,2.1002 1.6793,3.4854 2.4129,4.8545 5.4995,14.1279 6.6616,20.0131 2.785,14.1049 1.5763,35.4 -2.5863,45.5637 -0.1034,0.2528 -0.4773,1.3328 -0.8306,2.4002 -1.9693,5.9485 -5.6108,13.0478 -8.9706,17.4881 -3.5901,4.7449 -3.5745,4.7071 -2.5231,6.1377 1.5087,2.0529 5.1523,9.0393 6.1466,11.7859 0.2641,0.7295 0.7089,1.9657 0.9882,2.7472 0.2796,0.7816 0.7036,1.8925 0.9425,2.46876 0.2389,0.57626 0.7887,2.32405 1.2217,3.88399 0.4332,1.55993 0.9061,3.21991 1.0509,3.68884 0.3691,1.19458 0.6598,3.35446 1.2495,9.28367 1.1225,11.28504 0.3564,21.6401 -2.3901,32.30343 -0.7667,2.97684 -2.6423,8.57765 -3.5047,10.46575 -0.2017,0.44174 -0.3669,0.852 -0.3669,0.91169 0,0.36241 -4.4274,9.35514 -5.0324,10.22133 -0.291,0.41682 -0.9529,1.39729 -1.4708,2.17882 -2.6368,3.97864 -3.8477,5.45705 -7.9729,9.73351 -1.8786,1.9476 -1.9011,1.49234 0.2162,4.40819 0.6702,0.92315 1.5315,2.14737 1.9138,2.72049 1.2572,1.88443 4.372,6.30253 6.2112,8.81003 0.9937,1.35467 2.4763,3.44349 3.295,4.64185 0.8187,1.19834 2.5155,3.58558 3.7707,5.30494 3.5394,4.84808 5.8002,8.27771 6.6408,10.07382 4.1125,8.78693 -2.8311,23.35628 -16.3975,34.4058 -0.9895,0.80583 -2.1658,1.76354 -2.6142,2.12826 -6.0837,4.94792 -12.8528,8.95466 -19.8212,11.73254 -8.2134,3.27414 -11.0944,3.55091 -11.4915,1.10397 -0.2547,-1.56961 0.017,-2.05948 4.8305,-8.66833 1.4037,-1.92777 3.0215,-4.18712 3.5952,-5.02076 0.5737,-0.83363 1.5713,-2.28303 2.2168,-3.22087 1.0612,-1.54145 1.7115,-2.60302 4.6429,-7.57851 2.9165,-4.95017 5.4898,-11.05328 5.4898,-13.02015 0,-1.24229 -1.2524,-3.30859 -4.7051,-7.76369 -1.8358,-2.36875 -3.3099,-4.36196 -8.6593,-11.70906 -0.645,-0.88573 -2.4844,-3.55999 -4.0877,-5.94276 -3.5111,-5.21787 -2.6716,-4.99024 -9.4518,-2.56243 -1.1251,0.40291 -5.8005,1.2988 -8.2415,1.57925 -1.4589,0.16762 -3.2231,0.38776 -3.9205,0.48922 -2.7564,0.40097 -8.2369,0.16605 -13.6049,-0.58319 -2.2703,-0.31689 -6.6673,-1.46279 -9.9467,-2.59221 -4.2126,-1.45078 -3.9885,-1.45039 -5.0173,-0.009 -0.4669,0.65438 -1.49,2.01033 -2.2735,3.01322 -1.4235,1.82216 -3.3121,4.32005 -4.5369,6.00074 -0.3573,0.49011 -1.9772,2.55386 -3.5999,4.58611 -1.6227,2.03227 -3.1891,4.0145 -3.481,4.40497 -0.2918,0.39047 -0.9608,1.2002 -1.4865,1.7994 -0.8925,1.01738 -3.5659,4.47412 -4.7634,6.1593 -2.8314,3.98464 -2.114,7.76744 3.4537,18.21334 1.3598,2.55114 3.963,6.50495 8.4339,12.80951 5.5864,7.87782 6.0591,8.78903 5.3102,10.2372 -0.6582,1.27276 -1.589,1.36792 -4.6386,0.47424 z"
id="path21269" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1388.7652,-1007.5996 c -5.8227,-2.6259 -9.1991,-5.437 -11.9327,-9.9347 -0.3484,-0.5731 -1.2023,-1.9799 -1.8977,-3.126 -1.3115,-2.162 -4.3598,-8.3758 -5.2191,-10.6392 -1.282,-3.3764 -3.4016,-10.1595 -3.8827,-12.4249 -0.2051,-0.9655 -0.4216,-1.8835 -0.4812,-2.0398 -0.1639,-0.4303 -0.9986,-4.5519 -1.4196,-7.0101 -0.8001,-4.6732 -1.1514,-7.7036 -1.8892,-16.2938 -0.7911,-9.212 -0.2779,-27.3932 1.1474,-40.6399 0.112,-1.042 0.3283,-3.1719 0.4807,-4.733 0.1523,-1.5612 0.4449,-3.9485 0.6504,-5.305 0.2054,-1.3565 0.4598,-3.0633 0.5655,-3.7927 0.4804,-3.3151 1.5541,-9.6808 1.9816,-11.7468 0.7494,-3.623 1.428,-6.7493 1.6226,-7.4756 0.099,-0.3691 0.3904,-1.5664 0.6479,-2.6606 0.2574,-1.0941 0.7252,-3.055 1.0394,-4.3577 1.2841,-5.3225 1.5878,-5.1937 -7.3698,-3.1246 -10.1381,2.3418 -14.1671,3.4752 -20.5567,5.7826 -1.7715,0.6397 -4.1459,1.4961 -5.2765,1.903 -1.1305,0.4069 -2.7504,1.0467 -3.5997,1.4217 -0.8494,0.375 -1.8001,0.7918 -2.1127,0.9265 -1.5546,0.6693 -8.3608,3.7741 -8.5258,3.8894 -0.1042,0.073 -1.0421,0.5842 -2.0841,1.1366 -1.0421,0.5523 -2.0652,1.1097 -2.2735,1.2387 -0.2085,0.1289 -1.4448,0.7837 -2.7473,1.455 -1.3025,0.6713 -2.7093,1.4174 -3.1262,1.6581 -0.4167,0.2406 -1.7383,0.9519 -2.9366,1.5808 -1.1984,0.6289 -2.733,1.4821 -3.4103,1.8961 -2.6246,1.6041 -3.9572,2.3753 -5.8984,3.4146 -1.1078,0.593 -3.0877,1.7803 -4.3999,2.6385 -1.312,0.8582 -2.7928,1.8089 -3.2906,2.1126 -11.4464,6.9844 -29.4494,21.4049 -40.9311,32.7859 -5.9123,5.8603 -6.2292,6.0493 -7.4275,4.4286 -0.7969,-1.0778 -0.6741,-1.3984 2.0205,-5.2738 10.6149,-15.2674 32.1009,-37.4481 48.1056,-49.6614 1.1449,-0.8736 2.3333,-1.7822 2.6411,-2.0191 3.1702,-2.4402 4.511,-3.4358 5.4173,-4.0226 0.5877,-0.3804 1.3976,-0.948 1.8,-1.2612 0.4022,-0.3134 1.6693,-1.2092 2.8156,-1.9909 1.1462,-0.7817 2.894,-1.984 3.8839,-2.672 0.99,-0.688 2.4394,-1.6551 3.2209,-2.1492 0.7815,-0.4942 2.3172,-1.47 3.4125,-2.1685 1.0952,-0.6985 2.502,-1.5457 3.126,-1.8826 1.9664,-1.0615 3.1618,-1.7264 5.1135,-2.844 4.9429,-2.8307 15.9289,-7.9772 21.883,-10.2514 1.6151,-0.6169 3.1072,-1.2028 3.3156,-1.3019 1.451,-0.6899 6.0037,-2.3879 8.6205,-3.215 4.7239,-1.4933 4.8035,-1.5193 5.2102,-1.7075 1.2028,-0.5562 12.0225,-3.8689 15.0624,-4.6116 7.9785,-1.9496 12.6945,-0.5743 16.2248,4.7315 2.8387,4.266 2.9057,7.8163 0.2694,14.2737 -2.741,6.7145 -6.0927,16.1664 -6.8525,19.3252 -0.2131,0.8858 -0.5836,2.2499 -0.8235,3.0314 -0.2399,0.7815 -0.584,1.9752 -0.7646,2.6524 -0.1805,0.6774 -0.4,1.4447 -0.4875,1.7052 -0.1494,0.4448 -1.3994,5.5403 -1.9752,8.0522 -0.5596,2.4409 -1.2398,5.7822 -1.6007,7.8627 -0.2079,1.1984 -0.5029,2.8183 -0.6556,3.5998 -0.1527,0.7815 -0.4557,2.572 -0.6734,3.9787 -0.2178,1.4068 -0.4754,3.0694 -0.5725,3.6946 -0.097,0.6252 -0.223,1.4352 -0.2795,1.7999 -2.4243,15.6279 -2.8728,36.4364 -1.0455,48.5025 1.9607,12.9468 8.2616,27.6355 16.2343,37.8451 2.9208,3.7401 2.9562,3.9441 1.1076,6.3659 -0.635,0.8319 -1.6846,2.499 -3.4769,5.523 -1.7723,2.9903 -1.6432,2.9649 -5.7239,1.1246 z"
id="path21281" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1571.8105,-906.44907 c -1.0547,-0.65454 -1.3054,-1.68463 -0.94,-3.86175 0.2379,-1.41654 0.7097,-5.88837 1.1581,-10.97628 0.8133,-9.22935 1.067,-11.27594 2.4537,-19.79887 0.1013,-0.62523 0.3166,-1.94673 0.4777,-2.93667 0.4792,-2.9466 0.8115,-4.75966 1.236,-6.74439 0.2208,-1.0319 0.5684,-2.68613 0.7723,-3.67607 0.6246,-3.03085 2.6171,-10.75914 3.4192,-13.26241 1.6799,-5.24257 3.4547,-10.55742 3.7646,-11.27304 0.3425,-0.79122 2.0249,-5.06696 3.4713,-8.82247 0.4641,-1.2052 1.1407,-2.78248 1.5034,-3.50506 0.3627,-0.72259 1.1739,-2.55321 1.8027,-4.06804 0.6286,-1.51484 1.7153,-3.9447 2.4146,-5.39968 0.6993,-1.4551 1.7363,-3.6685 2.3045,-4.919 0.5682,-1.2504 1.4429,-3.0409 1.9438,-3.9787 0.5009,-0.9379 1.5935,-3.0267 2.4277,-4.6419 3.0705,-5.9442 6.3383,-11.2849 11.7084,-19.1358 1.8857,-2.7567 3.0674,-4.3946 4.7246,-6.5481 0.8336,-1.0834 1.8141,-2.3719 2.1788,-2.8635 4.1072,-5.5347 16.4116,-19.086 24.9999,-27.5336 12.9724,-12.7598 23.566,-21.8905 31.9564,-27.5434 0.6378,-0.4298 2.0871,-1.4313 3.2209,-2.2257 10.1055,-7.0808 16.533,-8.3386 21.8208,-4.2698 3.6021,2.7718 4.4487,4.9992 4.3681,11.4929 -0.1413,11.3874 0.1722,15.6696 1.7267,23.588 1.7288,8.8065 2.063,10.3445 2.4948,11.4807 0.1949,0.5133 0.3546,1.1347 0.3546,1.381 0,0.2464 0.1342,0.8209 0.2984,1.2769 0.1641,0.456 0.4973,1.4684 0.7404,2.2499 1.2782,4.1093 2.5916,7.5374 4.5583,11.8984 1.4749,3.2706 2.0342,4.4268 3.5472,7.3322 0.4882,0.9378 1.5167,3.0266 2.2853,4.6419 1.5516,3.2605 4.8531,9.2879 6.4109,11.7043 0.5561,0.8625 1.4799,2.3487 2.053,3.3027 3.7694,6.2741 13.5463,12.8354 22.5461,15.13059 3.196,0.81504 4.3536,1.55881 3.9753,2.55393 -0.1003,0.26379 -0.487,1.56324 -0.8591,2.88767 -0.3722,1.32442 -0.9655,3.26062 -1.3184,4.30266 -0.58,1.71309 -1.0603,3.36793 -1.6757,5.77369 -0.5482,2.14332 -6.7881,1.27333 -15.422,-2.1502 -8.0086,-3.17554 -17.6559,-12.92694 -27.2498,-27.54384 -4.4414,-6.7667 -7.3082,-11.5271 -9.2697,-15.392 -1.5617,-3.0775 -4.6293,-9.6326 -5.4654,-11.6792 -0.4625,-1.132 -1.1114,-2.6974 -1.4421,-3.4791 -2.766,-6.5376 -6.2829,-18.1636 -7.3074,-24.1564 -0.3002,-1.7559 -0.5918,-3.1052 -1.0543,-4.8788 -0.2984,-1.1444 -0.3933,-1.1092 -5.381,1.9983 -0.8336,0.5192 -1.8263,1.2222 -2.2059,1.5621 -0.3797,0.3397 -1.1469,0.914 -1.7051,1.2762 -0.5582,0.362 -1.3134,0.9325 -1.678,1.2676 -0.3648,0.3351 -1.6383,1.4328 -2.8301,2.4392 -2.658,2.2445 -5.3855,4.6523 -7.0221,6.1986 -0.6772,0.6401 -2.7217,2.5194 -4.5431,4.1761 -21.9692,19.9833 -41.2206,42.1321 -50.6218,58.24075 -0.5777,0.98994 -1.7789,3.05012 -2.6692,4.57818 -0.8904,1.52806 -2.6622,4.89576 -3.9375,7.48379 -1.2752,2.58803 -3.0553,6.19751 -3.9556,8.02109 -0.9004,1.82358 -2.0531,4.25344 -2.5616,5.3997 -0.5084,1.14624 -1.5101,3.30344 -2.2259,4.79376 -0.7159,1.49033 -1.6053,3.45127 -1.9763,4.35765 -0.615,1.50217 -0.9401,2.24076 -1.9767,4.48991 -0.5089,1.10425 -1.6261,3.89962 -2.3852,5.96809 -0.3633,0.98994 -0.945,2.52459 -1.2927,3.41033 -0.3476,0.88574 -0.9309,2.37776 -1.2961,3.3156 -0.8338,2.14107 -4.9012,14.44931 -5.4472,16.48327 -0.9205,3.42976 -3.2479,13.27335 -3.494,14.77811 -0.9547,5.83668 -1.8912,7.28064 -3.9095,6.028 z"
id="path21279" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1051.2372,-905.15276 c -1.2128,-0.7415 -1.3505,-1.20128 -3.0447,-10.16625 -0.256,-1.35467 -0.5894,-2.97457 -0.741,-3.5998 -0.1515,-0.62523 -0.5832,-2.45829 -0.9591,-4.07345 -0.6624,-2.84563 -1.7035,-6.50494 -3.0185,-10.60993 -0.3505,-1.09414 -1.035,-3.26823 -1.5211,-4.8313 -2.3498,-7.55702 -3.8122,-11.08546 -9.1874,-22.16716 -2.5982,-5.35645 -3.5948,-7.47553 -4.5895,-9.75733 -0.5224,-1.19836 -1.4012,-3.07404 -1.953,-4.16819 -0.5518,-1.09415 -1.5178,-3.05509 -2.1465,-4.35765 -0.6289,-1.30256 -1.8404,-3.60453 -2.6921,-5.11549 -0.8519,-1.51097 -2.3639,-4.19661 -3.3602,-5.96809 -5.4984,-9.77688 -8.1194,-14.0045 -11.6615,-18.8096 -3.7994,-5.154 -8.4351,-10.9504 -10.7163,-13.3991 -4.9116,-5.2723 -6.4436,-6.9063 -8.2561,-8.8064 -3.3825,-3.5455 -11.8124,-11.9301 -16.2939,-16.2061 -2.2914,-2.1864 -5.0623,-4.8313 -6.1575,-5.8774 -3.6359,-3.4732 -11.5809,-10.0951 -16.0115,-13.3453 -1.0421,-0.7644 -2.2818,-1.7105 -2.755,-2.1025 -0.8091,-0.6703 -4.9304,-3.3655 -6.4716,-4.2322 -1.3351,-0.7508 -2.1,0.4074 -2.8046,4.2462 -0.4155,2.2637 -1.4048,6.6847 -1.7172,7.6733 -0.099,0.3126 -0.4361,1.6768 -0.7495,3.0314 -0.3136,1.3547 -0.7805,3.1024 -1.0379,3.8839 -0.2573,0.7816 -0.8463,2.572 -1.3089,3.9788 -2.5234,7.6721 -5.3912,14.0913 -11.3421,25.388 -0.5214,0.9899 -1.4266,2.5913 -2.0114,3.5585 -0.5847,0.9673 -1.2283,2.033 -1.4302,2.3683 -0.6609,1.098 -2.8252,4.3842 -3.3339,5.0621 -0.2737,0.3647 -1.1661,1.6009 -1.9832,2.7472 -1.3797,1.9352 -3.5465,4.6994 -7.4859,9.5499 -5.7859,7.12376 -13.6661,13.4112 -20.0807,16.02195 -0.6773,0.27567 -1.8284,0.78419 -2.5578,1.13005 -0.7295,0.34584 -2.4345,0.9869 -3.7893,1.42456 -1.3546,0.43764 -2.6761,0.88618 -2.9366,0.99673 -2.5596,1.08615 -4.7828,0.35241 -5.1012,-1.68359 -0.1276,-0.81576 -0.3585,-1.95212 -0.5131,-2.52524 -0.1547,-0.57313 -0.4434,-1.63886 -0.6415,-2.36829 -2.6894,-9.89996 -2.675,-9.06013 -0.1708,-10.02123 3.9174,-1.50353 5.4635,-2.18474 8.4457,-3.72104 1.7878,-0.921 3.6299,-1.9572 4.0934,-2.3026 0.4636,-0.3453 1.6305,-1.1766 2.593,-1.8474 6.1024,-4.2521 11.0526,-10.4225 16.2206,-20.2192 0.5962,-1.1301 1.5781,-2.9499 2.182,-4.044 0.604,-1.0942 1.5389,-2.9698 2.0775,-4.1682 0.5386,-1.1984 1.5227,-3.2973 2.1868,-4.6643 0.6639,-1.3671 1.4526,-3.1574 1.7524,-3.9788 0.2999,-0.8212 0.9905,-2.6442 1.5346,-4.0509 1.1175,-2.8893 2.4311,-7.0308 3.0345,-9.5679 0.2232,-0.9379 0.6529,-2.6003 0.955,-3.6946 0.6533,-2.3661 1.7288,-7.6513 2.2556,-11.0834 0.7297,-4.755 1.3694,-10.6082 2.0191,-18.4727 0.4939,-5.9779 0.6948,-7.1517 1.5301,-8.939 2.5612,-5.4798 7.7868,-7.9638 13.906,-6.6103 1.5611,0.3453 8.495,3.9855 9.7521,5.1198 0.2085,0.188 1.7005,1.2135 3.3157,2.2789 1.6152,1.0655 3.1638,2.096 3.4413,2.2903 0.2776,0.1941 1.4712,1.0065 2.6525,1.8053 5.8384,3.9476 14.7552,11.1304 20.3363,16.3816 0.6252,0.5883 1.5204,1.4017 1.9893,1.8074 4.5567,3.9431 17.3336,17.3297 23.1693,24.275 8.5254,10.1465 13.9509,17.4905 18.5143,25.0611 0.6594,1.0941 1.5719,2.5712 2.0276,3.2825 0.731,1.1411 1.8009,3.044 2.6806,4.7677 0.1591,0.3116 0.4851,0.8552 0.7246,1.2082 0.4257,0.6273 5.0629,9.9065 7.3031,14.6139 0.6198,1.3025 1.7128,3.57013 2.4288,5.03911 2.0609,4.22846 5.1798,11.371 6.1555,14.0966 0.786,2.19598 0.9256,2.56314 2.053,5.39969 0.8712,2.19209 2.8402,7.95218 4.6628,13.64133 0.5074,1.58381 1.298,4.57484 1.8028,6.82066 0.2342,1.04205 0.7233,3.04562 1.0868,4.45238 1.5578,6.02916 2.2082,9.40886 3.4451,17.90424 0.6436,4.42041 1.2381,10.03592 1.5244,14.39919 0.3392,5.17256 0.7692,9.79107 1.1527,12.38379 0.2983,2.0167 0.1909,2.42655 -0.8699,3.31907 -0.6998,0.58886 -0.8569,0.60328 -1.6027,0.14728 z"
id="path21277" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1524.6154,-780.78748 c -0.8444,-0.46733 -4.9487,-8.46179 -7.1559,-13.9384 -0.735,-1.82358 -1.5662,-3.82715 -1.8471,-4.45239 -0.281,-0.62522 -0.7864,-1.86147 -1.1229,-2.7472 -0.3367,-0.88574 -1.0594,-2.76143 -1.6061,-4.16819 -2.6659,-6.86063 -6.3122,-18.68126 -7.393,-23.96706 -0.085,-0.41681 -0.4684,-2.20723 -0.8515,-3.97872 -0.3832,-1.77147 -0.8549,-4.07345 -1.0484,-5.11549 -0.1934,-1.04205 -0.5233,-2.74722 -0.7329,-3.78926 -0.4116,-2.04514 -1.1927,-7.49489 -1.5345,-10.70465 -0.1166,-1.09415 -0.2907,-2.67144 -0.3872,-3.50506 -1.5314,-13.23132 -2.0562,-45.40144 -0.855,-52.40895 0.7139,-4.16531 3.4229,-10.44385 7.3068,-16.93447 0.9977,-1.66728 2.2904,-3.84137 2.8726,-4.83132 4.2349,-7.19922 14.5483,-21.51103 19.64,-27.25427 0.6151,-0.69362 1.8315,-2.12942 2.7031,-3.19066 1.4483,-1.76311 3.2212,-3.82542 6.307,-7.33691 0.6333,-0.72063 1.4551,-1.65847 1.8263,-2.08408 8.5053,-9.75158 26.5817,-28.05284 32.7912,-33.19894 1.5106,-1.2519 4.2382,-3.2887 4.4042,-3.2887 0.043,0 0.6625,-0.3553 1.3766,-0.7896 6.5868,-4.0062 12.6237,-1.4734 16.9602,7.1156 6.1139,12.10981 22.6254,24.42593 38.0408,28.37518 4.6331,1.18692 12.7273,1.37594 18.1475,0.42379 3.5814,-0.62914 3.6764,-0.50378 3.8755,5.11353 0.082,2.30482 0.237,4.6595 0.3451,5.23262 0.985,5.22329 0.4784,5.83008 -6.0051,7.1936 -9.8694,2.0756 -18.3529,1.41914 -29.8049,-2.30637 -4.7285,-1.53823 -13.0235,-5.40727 -16.3323,-7.61777 -5.8468,-3.90622 -8.6799,-6.202 -14.3829,-11.65513 -7.092,-6.78131 -6.8261,-6.89037 -22.91,9.39259 -4.1669,4.21838 -8.2028,8.45933 -11.063,11.62525 -4.2273,4.67879 -14.137,16.6436 -16.8861,20.38803 -0.5267,0.71747 -2.441,3.26545 -4.254,5.66215 -2.7303,3.60937 -7.7461,10.91483 -11.9675,17.43059 -4.9454,7.63321 -7.4094,14.57972 -7.9173,22.32032 -0.5833,8.88979 -0.4109,35.28451 0.2749,42.09705 0.1469,1.45886 0.3577,4.05925 0.4685,5.77862 0.1899,2.94863 1.1437,12.08686 1.5423,14.77811 0.1004,0.67733 0.3497,2.42513 0.5541,3.88399 0.5027,3.58842 1.0897,7.22117 1.6993,10.51519 0.2025,1.09415 0.5476,2.96983 0.7669,4.16818 0.2195,1.19836 0.5981,3.03141 0.8416,4.07345 0.2435,1.04205 0.7998,3.42928 1.2361,5.30496 0.4363,1.87569 0.8719,3.66611 0.968,3.97873 0.096,0.31261 0.4355,1.50623 0.7544,2.65247 0.7699,2.76789 2.4393,7.86098 2.7325,8.33638 1.1206,1.81751 -0.6567,4.37589 -2.3779,3.42321 z"
id="path21267" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1097.9379,-779.80916 c -1.5217,-0.76164 -1.5219,-0.74568 0.099,-6.41702 2.0228,-7.07604 3.1431,-12.12399 4.2621,-19.20436 3.7688,-23.8461 4.9839,-39.66869 5.2306,-68.11191 0.1888,-21.77417 0.081,-22.38837 -6.0881,-34.54459 -4.493,-8.85397 -9.3296,-16.42058 -17.218,-26.93612 -2.8761,-3.8341 -8.9588,-11.40904 -11.4715,-14.28588 -1.3108,-1.50077 -2.98,-3.42996 -3.7094,-4.28712 -6.9198,-8.13138 -22.334,-24.34391 -24.94,-26.2317 -0.7498,-0.54319 -0.8815,-0.57166 -2.643,-0.57166 -2.616,0 -2.0764,-0.32663 -8.2068,4.96805 -1.0524,0.90885 -2.457,2.05983 -3.1216,2.55775 -0.6645,0.49791 -1.9325,1.48954 -2.8178,2.20361 -0.8852,0.71408 -2.6009,1.94393 -3.8125,2.73302 -1.2117,0.78909 -2.923,1.90263 -3.8031,2.47451 -1.3652,0.88717 -4.8952,2.76907 -9.3888,5.0053 -6.7795,3.37386 -12.2222,5.1714 -20.5567,6.78898 -2.9982,0.58192 -11.483,0.46369 -14.5886,-0.20329 -4.9485,-1.06274 -6.1602,-1.36976 -8.088,-2.04933 -4.2288,-1.49071 -3.9708,-0.90056 -3.7243,-8.52251 0.2764,-8.54566 0.028,-8.21009 5.4652,-7.37133 8.5133,1.31317 21.891,-0.86397 32.3035,-5.25721 7.8248,-3.30145 22.1897,-15.31752 26.6041,-22.25404 8.0597,-12.66469 13.6596,-13.81619 23.7486,-4.88339 0.6085,0.5388 1.5708,1.3633 2.1385,1.8322 0.5676,0.4689 2.3633,2.1741 3.9903,3.78924 1.6271,1.61517 3.8011,3.74663 4.8313,4.73658 1.8519,1.77949 7.212,7.363 8.8135,9.18089 0.8257,0.9371 1.5732,1.76305 4.8185,5.3235 3.3087,3.63027 5.4951,6.09566 6.9979,7.89087 3.4173,4.08236 6.8911,8.40952 9.475,11.80279 1.0316,1.35466 2.3484,3.07024 2.9262,3.81241 0.5779,0.74217 1.5622,2.05768 2.1874,2.92334 0.6252,0.86567 1.5545,2.13412 2.0651,2.81879 0.9338,1.25213 1.2132,1.6624 4.4851,6.58414 1.7856,2.6859 3.5028,5.38793 5.5773,8.77568 0.6062,0.98995 1.4196,2.26883 1.8076,2.84195 7.6515,11.30182 10.6721,18.58023 11.7548,28.3247 0.8511,7.65988 0.6759,22.00663 -0.4835,39.59775 -0.3185,4.83145 -0.7768,10.37169 -1.0503,12.69401 -0.8074,6.85862 -1.2343,9.74693 -2.4638,16.67274 -2.2601,12.73024 -4.7741,21.89077 -9.0472,32.96654 -2.2607,5.85946 -2.5652,6.57714 -5.063,11.93616 -0.1457,0.31262 -0.5649,1.22341 -0.9315,2.024 -0.7166,1.56464 -2.5404,4.84492 -3.4434,6.19326 -0.3051,0.45553 -0.6802,1.02595 -0.8336,1.26762 -0.2173,0.34249 -0.9448,0.8518 -1.1984,0.83889 -0.022,-10e-4 -0.4212,-0.19359 -0.8891,-0.42781 z"
id="path21265" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1372.6609,-723.60939 c -1.855,-1.08537 -4.8912,-2.66017 -6.0674,-3.14684 -0.3672,-0.15195 -1.305,-0.64271 -2.0841,-1.09057 -0.779,-0.44786 -1.7574,-1.00342 -2.1743,-1.23458 -14.7165,-8.16137 -26.5442,-17.60623 -38.668,-30.87807 -1.0471,-1.14624 -2.5693,-2.80878 -3.3827,-3.69452 -6.0598,-6.59929 -15.1394,-19.26533 -19.6251,-27.37738 -0.3458,-0.62524 -0.9971,-1.75268 -1.4472,-2.50545 -1.1856,-1.98224 -5.1055,-9.33118 -6.041,-11.32535 -4.9318,-10.51271 -5.3147,-11.36642 -6.799,-15.15703 -0.6936,-1.77148 -1.3829,-3.51928 -1.5316,-3.88399 -4.1146,-10.08764 -8.8787,-28.0974 -11.8529,-44.80798 -0.1113,-0.62522 -0.2824,-1.52044 -0.3802,-1.98935 -0.2293,-1.09929 -1.5686,-8.81206 -1.9575,-11.27305 -0.9185,-5.81186 -1.4482,-9.36218 -1.7234,-11.55054 -0.1697,-1.35097 -0.3385,-2.50452 -0.3748,-2.56345 -0.6387,-1.03341 -1.0457,-17.89571 -0.4939,-20.46299 1.4224,-6.61767 6.253,-10.04152 14.1674,-10.04152 5.877,0 13.7905,-0.52996 18.5345,-1.24122 8.7162,-1.30683 14.3143,-2.50047 20.854,-4.44658 2.0938,-0.62307 3.8675,-1.13287 3.9415,-1.13287 0.3724,0 6.2057,-2.27581 8.3605,-3.26173 1.3547,-0.61983 3.1451,-1.4396 3.9787,-1.82169 3.0726,-1.40835 9.8708,-5.08706 13.7795,-7.45654 3.1123,-1.88663 3.5776,-1.96379 4.2304,-0.70151 0.3327,0.64336 1.4487,3.19759 1.9891,4.55215 0.1927,0.48321 0.6158,1.42104 0.9401,2.08409 0.5875,1.20113 0.9563,2.03753 2.0651,4.68286 1.2859,3.06795 1.1036,3.3249 -6.1075,8.61006 -9.0907,6.66273 -18.42,11.26102 -29.117,14.35129 -0.6774,0.19567 -1.9562,0.57649 -2.8421,0.84625 -6.3059,1.92056 -12.5797,3.58867 -15.3463,4.08041 -1.0943,0.19447 -3.4814,0.66044 -5.3051,1.03548 -2.6726,0.54967 -3.9401,0.70737 -6.5365,0.81327 -3.6681,0.14963 -3.9083,1.71404 -2.2743,14.81475 0.2952,2.36737 0.4021,3.21795 0.9405,7.48378 0.4065,3.22214 0.9662,7.17231 1.4228,10.04154 0.2156,1.35466 0.4788,3.14509 0.5851,3.97872 0.1062,0.83364 0.2305,1.64359 0.2763,1.79991 0.083,0.28256 0.2587,1.38253 0.7418,4.64184 0.1389,0.93783 0.4251,2.55775 0.6358,3.59979 0.2106,1.04204 0.4899,2.53406 0.6203,3.3156 0.1306,0.78154 0.5298,2.82773 0.8872,4.54711 0.3574,1.71937 0.7294,3.59506 0.8267,4.16818 0.097,0.57314 0.3364,1.72411 0.5312,2.55775 0.1948,0.83364 0.5018,2.19777 0.6823,3.03141 0.4337,2.00305 1.7379,6.74135 2.2162,8.05217 0.3996,1.09491 1.1859,3.73481 1.4952,5.02076 0.1723,0.71552 0.5158,1.84549 1.4979,4.92605 0.2326,0.72942 0.49,1.58202 0.572,1.89462 0.082,0.31262 0.6755,2.06041 1.3187,3.88399 0.6432,1.82358 1.3351,3.84353 1.5376,4.48878 0.2024,0.64525 0.7493,2.09463 1.2155,3.22087 0.466,1.12623 0.8477,2.10569 0.8483,2.17657 6e-4,0.0709 0.2612,0.71032 0.5791,1.42097 0.5705,1.27492 0.7175,1.61726 1.897,4.41823 0.3291,0.78153 1.1807,2.57196 1.8924,3.97872 0.7116,1.40676 1.5829,3.19719 1.936,3.97872 0.3532,0.78154 1.0113,2.06042 1.4627,2.84195 0.4513,0.78153 1.4695,2.57196 2.2628,3.97872 6.593,11.69136 15.602,23.93781 25.7851,35.05063 9.5484,10.42028 15.4447,16.05809 25.8561,24.72247 2.2491,1.8717 2.3682,2.03268 2.3682,3.19815 0,2.09239 -0.9685,2.29585 -3.5997,0.75619 z"
id="path21263" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1254.2051,-722.65362 c -1.6526,-0.72227 -1.2581,-2.7138 0.8463,-4.27161 5.6438,-4.17796 13.9202,-11.64804 22.097,-19.94413 8.5309,-8.65531 18.7748,-20.97388 23.3325,-28.05765 0.4022,-0.62522 1.4323,-2.20251 2.2888,-3.50507 2.1285,-3.23636 5.2013,-8.11401 5.2013,-8.25616 0,-0.064 0.1595,-0.34274 0.3544,-0.61934 0.6114,-0.86732 4.4924,-8.53149 5.6755,-11.20756 0.6218,-1.40676 1.558,-3.46435 2.0803,-4.5724 0.9365,-1.98682 1.31,-2.90177 3.7029,-9.06893 1.4092,-3.63197 1.711,-4.49825 2.772,-7.95744 0.4314,-1.40676 0.8575,-2.7709 0.9467,-3.03141 0.2126,-0.62077 1.1774,-3.88455 1.68,-5.68389 0.4577,-1.6378 1.3455,-5.27987 1.8244,-7.48379 0.1811,-0.83363 0.584,-2.58143 0.8952,-3.88398 1.3142,-5.50072 2.6887,-12.23464 3.6546,-17.90425 0.2486,-1.45886 0.7154,-4.05923 1.0375,-5.77861 0.6987,-3.73001 1.1776,-6.37065 1.8043,-9.94681 0.2556,-1.45886 0.5553,-3.16403 0.6659,-3.78925 0.5375,-3.03905 2.0932,-12.96551 2.3606,-15.06231 1.6926,-13.27309 1.856,-12.4299 -2.4573,-12.68298 -3.3912,-0.19897 -5.997,-0.48133 -7.1034,-0.76973 -0.3648,-0.095 -2.3778,-0.51951 -4.4736,-0.94319 -19.2696,-3.89569 -36.4831,-10.59305 -48.1024,-18.7155 -0.7294,-0.50991 -2.5798,-1.79863 -4.112,-2.86384 -4.8618,-3.37978 -4.8587,-3.32882 -0.7619,-12.52725 3.0548,-6.85891 3.0497,-6.85713 9.1736,-3.16868 2.9739,1.79121 4.4591,2.61223 9.1522,5.05926 4.0054,2.08844 10.8696,4.92111 15.1571,6.25491 1.042,0.32417 2.5587,0.80325 3.3706,1.06461 3.549,1.14264 11.2705,2.95702 14.7569,3.46754 0.6588,0.0965 2.3487,0.34929 3.7554,0.56186 5.7738,0.8724 12.2027,1.30926 20.9356,1.42259 14.5004,0.18817 18.0672,5.91463 16.1219,25.88297 -1.3253,13.60387 -2.1984,19.80697 -3.9998,28.41943 -0.2506,1.19835 -0.7167,3.58558 -1.0356,5.30497 -0.319,1.71937 -0.8816,4.57552 -1.2504,6.34701 -0.7571,3.63751 -0.8708,4.12471 -3.582,15.34649 -0.9586,3.96733 -2.0322,7.74735 -3.6324,12.78873 -1.5436,4.86313 -1.8818,5.83664 -3.2246,9.28369 -0.6698,1.71938 -1.3444,3.48857 -1.4992,3.93155 -0.84,2.40519 -4.0902,9.73005 -5.7376,12.93065 -0.295,0.57312 -1.2563,2.44881 -2.136,4.16818 -0.8798,1.71938 -2.0745,3.93609 -2.6548,4.92604 -0.5803,0.98994 -1.7672,3.03614 -2.6374,4.5471 -3.5102,6.09423 -11.617,17.64916 -15.6188,22.26189 -0.5424,0.62524 -1.4675,1.72409 -2.0558,2.44192 -0.998,1.21775 -2.4563,2.85582 -5.4124,6.07945 -1.8797,2.04973 -7.949,8.23829 -9.0164,9.19341 -0.524,0.46893 -1.629,1.44939 -2.4554,2.17883 -0.8264,0.72943 -2.0431,1.81259 -2.7038,2.40701 -3.2543,2.92779 -8.4457,6.9981 -12.1927,9.55965 -1.1949,0.81686 -3.0218,2.06744 -4.0595,2.77906 -4.5775,3.13877 -8.3453,5.40371 -17.6123,10.58723 -5.5026,3.07787 -5.136,2.92803 -6.116,2.49973 z"
id="path21251" /><path
style="display:inline;fill:#f0b116;fill-opacity:1;stroke-width:0.71608"
d="m -1307.2962,-907.92432 c 5.8064,-0.59544 7.8147,-1.00764 12.5522,-2.57613 4.2944,-1.42185 8.87,-3.62563 14.2567,-6.86678 6.4938,-3.90724 13.6267,-11.43595 18.3424,-19.36028 0.1237,-0.20796 0.3647,-0.59162 0.5354,-0.85259 1.1448,-1.7502 3.829,-7.5377 5.0538,-10.89729 0.8322,-2.28253 1.2356,-3.72692 2.5122,-8.99712 1.8832,-7.77515 2.0463,-19.63405 0.3795,-27.61476 -1.9465,-9.32015 -4.8114,-17.20193 -7.8092,-21.48533 -0.1936,-0.2767 -0.352,-0.5489 -0.352,-0.605 0,-0.3455 -3.5929,-5.3288 -4.9875,-6.9177 -10.2926,-11.7266 -23.8495,-19.4263 -37.6416,-21.379 -5.448,-0.7713 -19.9353,-0.2318 -21.6137,0.805 -0.06,0.037 -0.953,0.2921 -1.9838,0.5664 -2.9979,0.7975 -7.2761,2.2951 -8.6112,3.0141 -0.6774,0.3648 -1.9561,1.0129 -2.842,1.4403 -2.5459,1.2283 -3.0101,1.4882 -5.0207,2.8107 -3.9566,2.6025 -12.4444,10.2311 -14.5716,13.0966 -0.4262,0.5741 -1.3347,1.7677 -2.0188,2.6524 -1.1516,1.4892 -3.398,4.9105 -3.398,5.1752 0,0.064 -0.325,0.6182 -0.7223,1.2329 -0.3973,0.6146 -0.8728,1.5341 -1.0569,2.0431 -0.1839,0.509 -0.7474,1.7355 -1.2522,2.72541 -1.9703,3.86443 -3.4839,8.25685 -4.4396,12.88347 -0.269,1.30256 -0.5862,2.83721 -0.705,3.41033 -0.8952,4.32026 -0.7684,16.13844 0.2338,21.78823 0.2125,1.19836 0.5122,2.90352 0.6659,3.78926 1.7048,9.82062 7.4186,22.39482 13.2584,29.17729 5.4937,6.38047 9.8672,10.1928 14.1301,12.31723 0.8698,0.43343 2.0598,1.0782 2.6446,1.43282 1.6125,0.97786 7.6407,3.87 8.0664,3.87 0.3801,0 1.8456,0.40791 4.4169,1.22939 0.7816,0.24968 2.0179,0.55478 2.7473,0.67801 0.7294,0.12324 1.4806,0.3044 1.6692,0.40259 1.1835,0.61617 11.7551,1.62025 14.2457,1.35304 0.2084,-0.0223 1.7003,-0.17617 3.3156,-0.34179 z"
id="path21272" /><path
style="display:inline;fill:#985289;fill-opacity:1;stroke-width:0.71608"
d="m -1217.7495,-1051.685 c 0.2256,-0.4349 0.3684,-1.4225 0.4858,-3.3629 0.091,-1.5109 0.2939,-3.6851 0.45,-4.8312 1.4151,-10.3986 1.5639,-26.4284 0.3735,-40.2652 -0.049,-0.5708 -0.1398,-1.8477 -0.2013,-2.8377 -0.061,-0.9899 -0.1502,-2.2262 -0.1968,-2.7472 -0.046,-0.521 -0.2147,-2.6951 -0.3734,-4.8314 -0.1587,-2.1361 -0.4987,-5.3759 -0.7555,-7.1995 -0.2568,-1.8236 -0.7225,-5.3619 -1.0348,-7.8628 -0.3124,-2.5009 -0.742,-5.5275 -0.9548,-6.7259 -0.5178,-2.9164 -1.277,-7.2826 -1.6306,-9.3783 -0.3249,-1.925 -0.8521,-4.5335 -1.3976,-6.9154 -0.589,-2.5717 -0.9336,-4.229 -1.2171,-5.8531 -0.1455,-0.8332 -0.4947,-2.0905 -0.7761,-2.7938 -1.7507,-4.3761 2.0554,-5.1067 9.0031,-3.9969 1.7764,0.2837 3.1253,0.6956 4.6444,1.0865 9.4816,2.7152 19.2492,5.0043 28.4168,8.7035 8.6259,3.4361 17.1419,7.2847 25.388,11.7429 2.375,1.2983 7.1398,4.4313 10.1363,6.6647 3.2272,2.4056 4.9495,2.6829 3.1139,0.5013 -0.2538,-0.3015 -3.3239,-3.2854 -4.8265,-4.4979 -0.4545,-0.3648 -1.0864,-0.8828 -1.4042,-1.1513 -0.3177,-0.2684 -1.459,-1.0784 -2.5361,-1.7999 -2.2452,-1.5038 -3.6749,-2.4889 -5.2628,-3.6262 -3.0312,-2.1712 -3.9258,-2.779 -5.846,-3.9724 -0.3572,-0.2221 -3.1891,-1.9679 -5.0476,-2.9577 -1.4671,-0.7814 -9.4465,-5.5474 -12.1993,-6.9444 -4.6782,-2.4616 -17.3614,-8.7704 -33.4401,-14.1486 -3.445,-1.1644 -5.9042,-1.8939 -9.189,-2.726 -2.0842,-0.5279 -4.2721,-1.0958 -4.8623,-1.2616 -6.4763,-1.8215 -10.378,2.9089 -7.7478,9.3934 0.1941,0.4785 0.4596,1.2963 0.5902,1.8173 0.1304,0.521 0.2999,1.0326 0.3764,1.1367 0.077,0.1043 0.4952,1.1274 0.9303,2.2736 0.4351,1.1463 0.9596,2.4417 1.1655,2.8786 0.206,0.4369 0.4883,1.2043 0.627,1.7052 0.3444,1.2421 1.3941,4.0412 1.6157,4.3081 0.099,0.1195 0.2614,0.5549 0.3605,0.9677 0.099,0.4127 0.4525,1.4751 0.7851,2.3607 0.3327,0.8859 0.7196,2.0368 0.8599,2.5579 0.4365,1.6219 1.1991,4.8734 1.4219,6.0627 0.1172,0.6252 0.3251,1.6483 0.4621,2.2736 0.1373,0.6252 0.3428,1.6909 0.4569,2.3683 0.4827,2.8633 1.1917,6.5772 1.3563,7.1048 0.1796,0.5756 0.5879,2.3891 1.4287,6.3471 0.8019,3.7744 2.0725,11.3353 2.6586,15.82 0.404,3.0914 0.3674,2.7931 0.534,4.3578 0.078,0.7294 0.2574,2.1361 0.3993,3.126 0.3641,2.5401 0.6153,4.679 0.8455,7.1996 0.2403,2.6305 0.3003,3.2443 0.6853,7.0102 0.4279,4.1849 0.5436,15.1848 0.2402,22.8303 -0.3113,7.8433 -0.2926,9.6345 0.1069,10.2443 0.4011,0.6121 0.5997,0.5808 0.9816,-0.1555 z"
id="path21284" /><path
style="display:inline;fill:#985289;fill-opacity:1;stroke-width:0.71608"
d="m -1405.616,-1053.4513 c 0.312,-0.376 0.2615,-1.4618 -0.1972,-4.2491 -1.7144,-10.4147 -0.6072,-29.6521 2.857,-49.6393 0.2257,-1.3025 0.5179,-3.1356 0.6494,-4.0733 0.8044,-5.7413 1.7895,-10.9696 2.8738,-15.2519 0.8421,-3.3257 1.0755,-4.319 1.7589,-7.4837 0.2025,-0.9379 0.6736,-2.984 1.0471,-4.5472 0.3733,-1.563 0.8543,-3.6945 1.0686,-4.7365 0.2316,-1.1251 0.657,-2.4333 1.0475,-3.2209 0.6361,-1.2826 1.3706,-3.3267 2.0427,-5.6839 0.1781,-0.6252 0.8395,-2.1598 1.4697,-3.4103 1.0647,-2.1129 1.478,-3.1615 3.6588,-9.2836 0.4454,-1.2505 0.9676,-2.5721 1.1605,-2.9367 0.9121,-1.7253 -0.01,-5.9466 -1.6528,-7.5927 -0.9669,-0.9668 -3.1907,-1.1914 -5.3857,-0.5439 -0.3126,0.092 -0.8668,0.2014 -1.2315,0.2427 -0.3647,0.042 -0.791,0.12 -0.9473,0.1747 -0.1563,0.054 -1.0515,0.276 -1.9894,0.4917 -1.807,0.4156 -2.1263,0.4833 -4.0735,0.8652 -10.4605,2.5271 -20.2397,7.0816 -30.2154,11.0148 -3.2145,1.0811 -6.1141,2.673 -9.0981,4.2484 -0.8336,0.4398 -1.7288,0.9361 -1.9894,1.1028 -0.2605,0.1668 -0.8146,0.4857 -1.2315,0.7087 -0.4168,0.2231 -1.1415,0.6227 -1.6104,0.8881 -0.4689,0.2656 -1.492,0.8452 -2.2735,1.2879 -0.7815,0.4429 -1.8047,1.0081 -2.2736,1.2559 -0.469,0.2478 -1.1935,0.6628 -1.6104,0.9222 -0.4169,0.2594 -1.0573,0.6329 -1.4231,0.8301 -0.3659,0.1974 -1.374,0.7936 -2.2401,1.3252 -0.8662,0.5317 -1.6123,0.9665 -1.6579,0.9665 -0.1062,0 -2.1987,1.3464 -5.0994,3.281 -2.1815,1.455 -3.5593,2.5103 -6.4417,4.9337 -0.7295,0.6132 -2.0243,1.5682 -2.8774,2.1221 -0.8531,0.5539 -1.7269,1.1616 -1.942,1.3506 -4.433,3.8951 -2.7137,4.7633 2.1123,1.0667 2.6573,-2.0354 12.8868,-7.643 17.201,-9.4292 0.469,-0.1942 1.2788,-0.5458 1.8,-0.7815 0.3047,-0.138 8.464,-3.4405 9.7572,-4.0201 4.0687,-1.7679 16.6441,-5.573 22.1672,-7.0412 1.6151,-0.4295 3.4056,-0.9169 3.9787,-1.0834 1.5444,-0.4486 4.7972,-1.1271 8.4311,-1.7585 1.7715,-0.3078 3.9029,-0.6917 4.7367,-0.8532 5.3344,-1.0334 6.3323,0.7796 4.2707,7.7603 -0.092,0.3126 -0.3121,1.2078 -0.4884,1.9893 -0.1763,0.7815 -0.6866,2.7426 -1.1338,4.3576 -0.9973,3.6025 -1.1421,4.217 -1.6965,7.1997 -0.2422,1.3025 -0.9077,4.6702 -1.479,7.4838 -0.5715,2.8135 -1.2244,6.2239 -1.451,7.5784 -0.2267,1.3548 -0.535,3.1878 -0.6855,4.0736 -0.1503,0.8856 -0.3532,2.2072 -0.4508,2.9366 -0.098,0.7295 -0.3958,2.7486 -0.6626,4.487 -0.2666,1.7384 -0.6078,4.4667 -0.7582,6.0628 -0.1505,1.5961 -0.4463,4.5219 -0.6574,6.5018 -0.9168,8.5943 -1.3076,26.2731 -0.6983,31.5831 0.063,0.5525 0.2378,2.8376 0.3875,5.078 0.1631,2.442 0.3949,4.4783 0.5788,5.0844 0.1688,0.556 0.3763,1.8777 0.461,2.937 0.1454,1.8158 0.267,2.2176 1.0851,3.5833 0.1483,0.2476 0.7646,0.1535 1.0215,-0.156 z"
id="path21282" /><path
style="display:inline;fill:#985289;fill-opacity:1;stroke-width:0.71608"
d="m -1548.2398,-987.38482 c 0.4745,-0.50736 1.1295,-1.38126 1.4553,-1.94199 0.3259,-0.56074 0.6691,-1.14741 0.7625,-1.30371 0.093,-0.1563 0.324,-0.66636 0.5125,-1.13346 1.4925,-3.69977 4.3377,-8.80252 7.7073,-13.82282 0.7949,-1.1842 1.4453,-2.1794 1.4453,-2.2115 0,-0.032 1.1724,-1.6307 2.6051,-3.5523 1.4328,-1.9217 2.733,-3.6844 2.8894,-3.9172 0.3416,-0.5087 4.2475,-5.3849 4.8508,-6.0557 0.2341,-0.2605 0.8366,-0.9426 1.3386,-1.5157 0.502,-0.5731 1.5163,-1.6815 2.2538,-2.463 0.7375,-0.7816 2.195,-2.4015 3.239,-3.5998 1.0438,-1.1984 2.6577,-2.9661 3.5863,-3.9283 5.9289,-6.1435 8.8047,-9.0873 9.5511,-9.7771 0.4689,-0.4334 2.2603,-2.1523 3.9808,-3.8196 1.7205,-1.6675 3.6509,-3.5028 4.2898,-4.0786 0.6389,-0.5759 1.5449,-1.5176 2.0135,-2.0928 0.4685,-0.5753 1.3515,-1.423 1.9622,-1.884 0.6106,-0.4611 1.6219,-1.3415 2.2473,-1.9564 0.6254,-0.6149 1.8772,-1.7832 2.7818,-2.5962 0.9046,-0.813 2.226,-2.0264 2.9367,-2.6963 0.7105,-0.6701 2.2298,-2.0191 3.376,-2.9977 1.1462,-0.9786 2.4007,-2.0678 2.7878,-2.4202 0.3869,-0.3525 0.952,-0.8219 1.2557,-1.0431 0.3036,-0.2211 1.4477,-1.1269 2.5425,-2.0126 1.0948,-0.8858 2.4054,-1.9262 2.9125,-2.312 0.507,-0.3859 1.3908,-1.0671 1.964,-1.5141 10.6223,-8.2831 14.2924,-10.0768 17.0767,-8.3453 1.0385,0.6458 1.6801,3.2202 1.6801,6.7414 0,0.8713 0.077,1.8313 0.17,2.1335 0.093,0.3021 0.2756,1.1888 0.4049,1.9703 0.4971,3.0072 1.2964,6.6138 1.8688,8.431 0.5662,1.7981 1.1631,3.9151 1.8163,6.4418 0.1616,0.6252 0.3776,1.2848 0.48,1.4657 0.1024,0.181 0.186,0.5017 0.186,0.7126 0,0.211 0.1635,0.745 0.3631,1.1867 0.1997,0.4418 0.5522,1.3573 0.7832,2.0346 0.2311,0.6774 0.5718,1.5726 0.7568,1.9894 0.1853,0.4168 0.3875,1.0563 0.4494,1.4209 0.062,0.3648 0.2474,0.919 0.4119,1.2316 0.1647,0.3126 0.6363,1.4515 1.0482,2.5309 0.4118,1.0793 1.2204,2.9075 1.7966,4.0624 0.5763,1.155 1.2567,2.5962 1.5119,3.2027 0.8664,2.0582 5.8144,10.7252 6.3599,11.1399 0.069,0.052 0.6232,0.8781 1.2322,1.8353 1.0459,1.6441 3.2449,4.9699 4.1106,6.2166 1.3619,1.9615 3.5329,5.2228 3.5329,5.3074 0,0.055 0.4095,0.6315 0.9099,1.2805 0.5005,0.649 1.0434,1.3933 1.2064,1.6538 0.163,0.2605 0.8682,1.1557 1.567,1.9893 0.6988,0.8336 1.4347,1.8317 1.6353,2.2181 0.416,0.8012 1.7336,1.6659 2.5384,1.6659 0.705,0 0.5911,-1.1844 -0.2141,-2.2259 -0.6366,-0.8234 -0.814,-1.0771 -2.0387,-2.9129 -0.5129,-0.7687 -1.0817,-1.5682 -1.2642,-1.7766 -0.6259,-0.7149 -3.4461,-5.3512 -4.7252,-7.768 -0.5516,-1.042 -1.2021,-2.1915 -1.4456,-2.5543 -0.2435,-0.3629 -0.4428,-0.753 -0.4428,-0.8671 0,-0.1141 -0.3624,-0.8829 -0.8053,-1.7085 -1.4465,-2.6963 -2.6186,-5.2026 -3.1656,-6.769 -0.5311,-1.5206 -2.0645,-5.4653 -2.3713,-6.1001 -0.1512,-0.3126 -0.4399,-0.9946 -0.6418,-1.5157 -0.2017,-0.521 -0.6368,-1.571 -0.9668,-2.3334 -0.3299,-0.7623 -0.9714,-2.4249 -1.4253,-3.6945 -1.3543,-3.7874 -1.7917,-4.9356 -2.0703,-5.4345 -0.2673,-0.4786 -0.6724,-1.8022 -1.1886,-3.8841 -0.155,-0.6251 -0.4853,-1.8188 -0.734,-2.6524 -0.2487,-0.8337 -0.7134,-2.624 -1.0328,-3.9788 -0.3193,-1.3545 -0.6723,-2.804 -0.7843,-3.2208 -0.2455,-0.9138 -0.2426,-0.8952 -0.6278,-3.9787 -0.1692,-1.3547 -0.3864,-2.7614 -0.4826,-3.1262 -0.096,-0.3647 -0.2194,-1.1319 -0.2734,-1.7052 -0.1823,-1.9329 -0.6274,-8.2332 -0.6559,-9.2836 -0.015,-0.5732 -0.1149,-2.193 -0.221,-3.5998 -0.106,-1.4068 -0.1805,-3.5808 -0.1655,-4.8312 0.037,-3.0537 -0.1931,-7.1112 -0.4291,-7.5787 -0.3257,-0.6454 -1.7529,-1.8512 -2.0699,-1.9893 -0.4611,-0.201 -0.7996,-0.6909 -2.9084,-0.59 -1.7376,0.083 -6.6203,2.0008 -9.5065,3.7338 -2.0191,1.2123 -3.3798,1.9813 -3.5452,2.1164 -0.8281,0.6766 -1.6979,1.2016 -2.8117,1.9904 -1.0874,0.7703 -1.4924,1.1562 -2.1879,1.7326 -0.5965,0.5643 -1.7239,1.5209 -2.5054,2.1258 -0.7816,0.6048 -1.5179,1.1877 -1.6363,1.2951 -0.8827,0.8002 -2.4055,2.0442 -3.546,2.8971 -0.7449,0.5569 -1.8232,1.4604 -2.3963,2.0076 -0.573,0.5472 -1.3404,1.1971 -1.7051,1.4442 -0.3647,0.2471 -1.1431,0.8819 -1.7297,1.4107 -0.5867,0.5288 -1.7375,1.5559 -2.5578,2.2823 -0.8201,0.7265 -2.0879,1.9457 -2.8174,2.7094 -2.6641,2.7895 -6.3887,6.4937 -7.1858,7.1469 -0.9144,0.7492 -6.9603,6.815 -9.2974,9.3279 -0.8337,0.8963 -2.5815,2.723 -3.884,4.0595 -2.9889,3.0664 -4.507,4.7225 -5.4943,5.9935 -0.4281,0.5511 -1.1201,1.3184 -1.538,1.7052 -0.4178,0.3869 -0.8763,0.9164 -1.0191,1.1769 -0.1426,0.2606 -0.9917,1.3263 -1.8869,2.3683 -4.7636,5.5451 -6.7131,8.0627 -9.6831,12.5046 -0.9059,1.3546 -1.9344,2.858 -2.2858,3.341 -0.3514,0.4829 -0.6394,0.9518 -0.6399,1.042 -4e-4,0.09 -0.4907,0.8147 -1.0893,1.6099 -0.5988,0.7951 -1.6427,2.3954 -2.3201,3.556 -0.6773,1.1606 -1.9354,3.3078 -2.7955,4.7715 -1.3339,2.2696 -6.0445,11.5795 -7.7089,15.2358 -0.2847,0.6252 -0.7597,1.6483 -1.0557,2.27353 -3.1557,6.66548 -4.7319,10.2463 -4.7328,10.75201 0,0.80537 0.5779,0.65706 1.5681,-0.40146 z"
id="path21280" /><path
style="display:inline;fill:#985289;fill-opacity:1;stroke-width:0.71608"
d="m -1073.8029,-987.09522 c -0.2085,-1.27219 -0.482,-2.2738 -0.8278,-3.03123 -0.198,-0.43355 -0.4916,-1.08666 -0.6523,-1.45138 -0.1609,-0.36472 -0.3727,-0.79101 -0.4707,-0.94731 -0.098,-0.15631 -0.5328,-1.05153 -0.9664,-1.98937 -1.1716,-2.45971 -2.0579,-4.97585 -3.5356,-7.29429 -0.121,-0.1564 -0.6584,-1.1795 -1.1942,-2.2736 -1.1727,-2.3951 -1.3912,-2.8022 -2.4028,-4.4774 -0.4321,-0.7157 -1.3597,-2.4096 -2.0612,-3.7643 -0.7014,-1.3546 -1.3377,-2.5482 -1.4138,-2.6524 -0.076,-0.1042 -0.4842,-0.8289 -0.9067,-1.6105 -0.4225,-0.7815 -1.0683,-1.9325 -1.4351,-2.5577 -0.3669,-0.6252 -0.8386,-1.4997 -1.0482,-1.9432 -0.5408,-1.1442 -5.7659,-9.0685 -7.3667,-11.1723 -0.4936,-0.6486 -1.5795,-2.1298 -2.4131,-3.2915 -0.8336,-1.1617 -1.7159,-2.3361 -1.9607,-2.6099 -0.2448,-0.2737 -0.8203,-1.0669 -1.2789,-1.7624 -0.4587,-0.6956 -1.7097,-2.2303 -2.78,-3.4104 -1.8795,-2.0723 -3.5959,-4.0701 -4.7798,-5.5637 -2.1676,-2.7347 -14.2706,-15.5588 -17.1359,-18.1572 -0.359,-0.3255 -2.2748,-2.2119 -4.2573,-4.1917 -1.9826,-1.9799 -4.1745,-4.0687 -4.8711,-4.6418 -0.6965,-0.5732 -2.5621,-2.1321 -4.1457,-3.4643 -2.4687,-2.0769 -11.4131,-8.9561 -14.171,-10.8988 -0.4895,-0.3449 -1.2419,-0.9418 -1.6717,-1.3263 -0.4299,-0.3845 -1.2825,-0.9929 -1.8947,-1.3519 -2.7876,-2.0959 -5.5744,-3.6163 -8.756,-4.8618 -3.8438,-1.5265 -6.4561,-1.3706 -8.3341,0.4975 -1.1404,1.1343 -1.1133,0.9971 -1.2673,6.438 -0.1196,4.2279 -0.3708,8.1391 -0.6606,10.2852 -0.1014,0.7518 -0.2222,1.6652 -0.2684,2.0299 -0.046,0.3647 -0.2574,1.8567 -0.4694,3.3156 -0.2119,1.4589 -0.4782,3.4197 -0.5917,4.3576 -0.1136,0.9379 -0.3264,2.2168 -0.473,2.842 -0.1467,0.6252 -0.531,2.5435 -0.8542,4.2629 -0.9956,5.2981 -2.8717,11.6423 -5.4067,18.2831 -0.2784,0.7295 -0.7381,1.9657 -1.0214,2.7473 -0.5141,1.4179 -2.1056,5.4595 -2.5739,6.5364 -0.136,0.3127 -0.5414,1.2505 -0.9011,2.0842 -0.3595,0.8336 -0.8263,1.8993 -1.0372,2.3682 -0.2109,0.469 -0.9542,2.1314 -1.6518,3.6946 -1.141,2.5568 -2.3999,5.0735 -5.1179,10.231 -0.9403,1.7843 -1.3125,2.417 -2.7943,4.7515 -0.495,0.7797 -0.9,1.4581 -0.9,1.5074 0,0.049 -0.4476,0.6788 -0.9946,1.3988 -1.1682,1.5376 -1.6478,2.1887 -2.0369,2.7652 -0.1561,0.2316 -0.5136,0.7521 -0.7941,1.1566 -0.7201,1.0387 -0.7861,1.8723 -0.1483,1.8723 0.464,0 3.0386,-2.4983 4.0687,-3.9482 0.2084,-0.2933 0.5494,-0.7289 0.7578,-0.9679 0.784,-0.8992 1.0344,-1.2438 1.3651,-1.8798 0.187,-0.3597 0.6955,-1.0665 1.1299,-1.5709 0.4344,-0.5043 1.0739,-1.3128 1.4209,-1.7968 0.3472,-0.4839 1.2019,-1.6349 1.8995,-2.5577 1.25,-1.6537 2.5486,-3.4145 3.613,-4.8987 0.5239,-0.7308 2.1255,-3.1219 3.5922,-5.3635 0.6158,-0.9411 3.298,-5.3098 3.7447,-6.0991 0.088,-0.1562 0.7721,-1.5204 1.5194,-3.0313 0.7472,-1.511 1.4202,-2.8325 1.4955,-2.9368 0.075,-0.1041 0.3796,-0.8288 0.6764,-1.6103 0.47,-1.2376 1.2426,-3.0953 2.0881,-5.0208 0.6611,-1.5059 0.9385,-2.2065 1.4623,-3.6946 1.056,-2.999 1.4437,-4.07 1.9545,-5.3996 0.7763,-2.0199 1.8005,-5.35 2.0688,-6.726 0.132,-0.6773 0.3756,-1.743 0.5413,-2.3683 0.9102,-3.4354 1.4072,-6.5059 1.5487,-9.5678 0.2417,-5.2331 0.8061,-7.1291 2.3278,-7.8203 2.1755,-0.9882 5.1064,-0.1505 8.7253,2.4935 3.0894,2.2574 4.5547,3.2503 5.3397,3.6187 0.469,0.22 1.1937,0.7328 1.6105,1.1398 0.4168,0.4069 1.0754,0.8719 1.4634,1.0335 0.8381,0.3488 4.774,3.6266 5.6935,4.257 0.4031,0.2763 0.62,0.4798 0.7922,0.5827 0.1288,0.077 0.7013,0.4507 1.3721,0.9034 0.6046,0.4969 1.3124,0.9973 1.5729,1.1119 0.2605,0.1144 0.9,0.5835 1.421,1.0423 0.521,0.4588 1.2883,1.0644 1.7052,1.3459 0.6838,0.4617 2.4522,1.9606 5.968,5.0582 0.6773,0.5968 1.9562,1.7076 2.8419,2.4685 0.8858,0.7611 3.0738,2.8429 4.8624,4.6262 1.7885,1.7834 5.6219,5.5871 8.5187,8.4528 7.1129,7.0364 11.0669,11.2398 13.9759,14.857 0.5098,0.6339 1.4476,1.7113 2.0841,2.3943 1.712,1.8371 2.2126,2.4194 3.2492,3.779 0.5163,0.6773 1.4931,1.8413 2.1706,2.5866 1.7833,2.585 4.0616,4.8036 5.7789,7.4295 0.1043,0.1633 0.5732,0.8812 1.0421,1.5954 0.4689,0.7144 0.9378,1.446 1.0421,1.6261 0.1041,0.1802 0.4546,0.6644 0.7788,1.0762 1.696,2.9736 3.6613,5.7482 5.3849,8.70596 0.316,0.52102 0.7207,1.24571 0.8993,1.61044 0.3263,0.66622 1.3013,1.94784 3.5274,6.347 2.1518,4.25234 3.9333,6.42391 3.5996,4.38788 z"
id="path21278" /><path
style="display:inline;fill:#812973;fill-opacity:1;stroke-width:0.71608"
d="m -1257.546,-1026.1934 c 0.6031,-0.6361 0.9273,-1.0595 1.8353,-2.3965 0.8999,-1.3251 4.7147,-8.2521 5.1155,-9.2887 0.141,-0.3646 0.319,-0.7483 0.3956,-0.8526 0.077,-0.1041 0.4471,-0.9993 0.8234,-1.9893 0.3763,-0.9899 0.8463,-2.2262 1.0446,-2.7472 4.7917,-12.5948 6.1783,-29.6549 3.357,-41.3029 -0.9629,-3.9756 -1.1208,-4.5922 -1.5762,-6.1575 -0.2576,-0.8857 -0.855,-2.8041 -1.3274,-4.2629 -0.4724,-1.4588 -0.9704,-3.0361 -1.1066,-3.5051 -2.0915,-7.2017 -5.9507,-14.1466 -11.3704,-20.462 -9.2906,-10.8261 -19.217,-18.4629 -29.3973,-22.6161 -4.136,-1.974 -8.4005,-3.4284 -12.8558,-4.5535 -3.473,-0.8336 -13.2154,-0.8312 -16.5691,0 -0.5731,0.1428 -1.4257,0.3521 -1.8946,0.4652 -1.9029,0.4591 -5.2317,1.3855 -5.5516,1.5451 -0.1878,0.093 -0.9746,0.336 -1.7487,0.5388 -0.774,0.2026 -1.7714,0.5684 -2.2164,0.8126 -0.445,0.2443 -1.4209,0.675 -2.1688,0.9571 -0.7479,0.2821 -1.4148,0.7082 -1.5985,0.8005 -1.0377,0.5218 -2.6866,1.1875 -3.5783,1.7035 -0.4689,0.2714 -1.5772,0.8799 -2.4629,1.3523 -0.8858,0.4725 -1.7384,0.9639 -1.8947,1.092 -0.1563,0.1282 -1.3926,0.9409 -2.7471,1.8059 -2.5197,1.6087 -4.4044,3.046 -6.9091,5.2683 -0.7789,0.6911 -1.845,1.6335 -2.3691,2.0942 -1.4987,1.3175 -5.6598,5.6044 -7.4854,7.7117 -3.4825,4.0196 -6.8733,9.1388 -8.9651,13.5349 -3.5094,7.3748 -5.0589,11.7482 -7.2125,20.3554 -1.2036,4.8107 -1.6275,9.3482 -1.6285,17.4305 -7e-4,5.3576 0.05,6.1544 0.7294,11.4625 0.623,4.868 2.3114,11.5479 3.7114,14.6835 0.2327,0.521 0.5262,1.2883 0.6522,1.7052 0.3681,1.2176 0.7938,2.1783 1.6374,3.6944 0.4348,0.7816 0.9873,1.9094 1.2277,2.5063 0.2404,0.5969 1.0001,1.7904 1.688,2.6524 1.562,1.9569 2.3013,3.3245 2.9505,4.1498 1.6195,2.0588 1.8123,2.3259 4.1569,-0.085 1.0457,-1.0751 2.7112,-2.5944 3.7012,-3.3763 0.9899,-0.7819 1.9276,-1.544 2.084,-1.6934 2.1254,-2.032 12.4121,-7.9433 13.8227,-7.9433 0.1169,0 0.529,-0.1598 0.9156,-0.3553 0.3866,-0.1955 1.8539,-0.7509 3.2607,-1.2344 1.4067,-0.4836 3.1546,-1.0963 3.884,-1.3618 0.7294,-0.2655 1.8378,-0.6413 2.463,-0.8352 0.6253,-0.1939 1.5205,-0.4799 1.9893,-0.6356 1.3066,-0.4338 4.825,-1.2345 6.726,-1.5307 0.9379,-0.1461 2.0035,-0.3593 2.3683,-0.4738 2.3607,-0.741 15.5243,-0.8001 19.7041,-0.088 5.1718,0.8807 6.0151,1.065 11.1783,2.4441 1.9253,0.5143 2.6047,0.7456 4.7366,1.6127 1.0942,0.4449 2.5009,1.0162 3.1262,1.2694 1.8824,0.762 7.4729,3.6337 8.7127,4.4754 0.6378,0.4329 2.3872,1.5675 3.1188,2.1031 1.7928,1.3128 3.0385,2.1835 4.5487,3.3861 0.5486,0.4371 1.3362,1.1459 1.8064,1.5817 0.4702,0.4357 1.0741,0.962 1.9411,1.6489 1.6058,1.272 2.1852,1.7286 2.7442,2.1014 1.2472,0.8317 1.6522,1.6687 2.4773,0.7985 z"
id="path21276-14-5" /><path
style="display:inline;fill:#6d2361;fill-opacity:1;stroke-width:0.71608"
d="m -1266.7895,-849.43329 c 0.5355,-0.39077 1.6797,-1.21739 2.5426,-1.83693 0.8631,-0.61953 2.1659,-1.60301 2.8954,-2.18549 0.7294,-0.58249 1.5448,-1.19686 1.8121,-1.36527 1.6596,-1.04601 4.7513,-4.36249 7.4678,-8.01086 0.4807,-0.64546 0.9728,-1.25106 3.0844,-3.79521 0.1729,-0.20841 0.6589,-0.89545 1.0799,-1.52675 5.1167,-7.67235 4.8088,-9.09303 -4.3032,-19.86164 -1.2246,-1.44735 -2.7426,-3.35301 -3.3734,-4.2348 -0.6308,-0.8818 -1.2281,-1.70117 -1.3273,-1.82084 -1.2908,-1.55591 -8.5973,-11.73258 -10.0137,-13.94728 -1.5488,-2.42196 -1.2465,-2.51023 -9.5836,2.79801 -0.8337,0.53077 -2.0995,1.32539 -2.8133,1.76581 -1.5579,0.96155 -1.6837,2.29848 -0.3186,3.38729 0.065,0.0521 0.3236,0.39313 0.5741,0.75785 0.2505,0.36472 0.5029,0.70575 0.561,0.75785 0.058,0.0521 0.4422,0.64891 0.8537,1.32624 0.4114,0.67733 1.0997,1.70043 1.5296,2.27355 0.4299,0.57312 0.839,1.16993 0.9093,1.32624 0.071,0.15631 0.5102,0.75311 0.9775,1.32624 1.6455,2.01797 3.4702,4.47538 4.9239,6.6312 0.8081,1.19835 1.5272,2.22145 1.5982,2.27355 0.1737,0.12754 2.6671,3.96245 2.6671,4.10199 0,0.0606 0.3197,0.54508 0.7105,1.07665 0.3907,0.53156 0.8064,1.13979 0.9235,1.35162 0.1173,0.21183 0.6434,1.09888 1.1693,1.97123 1.6317,2.70629 1.8455,5.95544 1.019,7.79233 -1.4201,3.15641 -2.539,6.52626 -3.7065,9.82355 -0.2724,0.76585 -1.0478,2.48736 -1.7231,3.82559 -2.4441,4.84341 -2.474,5.72407 -0.1362,4.01828 z"
id="path21271" /><path
style="display:inline;fill:#6d2361;fill-opacity:1;stroke-width:0.71608"
d="m -1360.5688,-848.69619 c 0.035,-0.2338 -0.1948,-0.80398 -0.521,-1.2965 -0.3203,-0.48356 -0.504,-1.04067 -0.5824,-1.19682 -1.0557,-2.10404 -2.0403,-5.59536 -3.2309,-9.2115 -0.8697,-3.39263 -1.0995,-4.44692 -1.5056,-8.25149 -0.2906,-2.72343 0.7082,-4.66206 2.8473,-7.73189 1.1187,-1.60539 2.5854,-3.78534 4.8441,-7.1996 0.6549,-0.98995 1.4089,-2.04762 1.6758,-2.35039 0.2666,-0.30277 0.4872,-0.60117 0.49,-0.66312 0,-0.062 0.3516,-0.49091 0.7751,-0.95328 0.4237,-0.46234 0.9969,-1.14274 1.2739,-1.51196 0.277,-0.36923 0.6446,-0.84351 0.8168,-1.05398 0.1723,-0.21047 0.8531,-1.06473 1.513,-1.89837 0.6599,-0.83364 1.2862,-1.61877 1.3919,-1.74474 0.1058,-0.12596 0.4906,-0.62397 0.8555,-1.10666 0.3646,-0.48269 1.7927,-2.22918 3.1733,-3.88106 3.0229,-3.61653 3.0584,-3.95483 0.516,-4.92144 -0.2531,-0.0962 -1.3293,-0.75331 -2.3915,-1.46014 -1.0621,-0.70681 -2.8181,-1.85314 -3.9022,-2.54738 -1.084,-0.69424 -2.1138,-1.38298 -2.2884,-1.53052 -1.2114,-1.02354 -2.1925,-1.28463 -2.5421,-0.67648 -0.9294,1.61649 -3.2702,4.8717 -6.2417,8.67948 -0.4473,0.57313 -1.6135,2.10778 -2.5916,3.41032 -0.9781,1.30256 -1.984,2.62407 -2.2351,2.93667 -0.2511,0.31263 -0.6285,0.78154 -0.8386,1.04206 -0.572,0.70951 -2.0223,2.38107 -3.2786,3.77877 -0.6141,0.6831 -1.8495,2.1372 -2.7455,3.23135 -0.8959,1.09415 -1.9,2.28776 -2.2311,2.65248 -0.3312,0.36472 -0.798,0.9189 -1.0374,1.23151 -0.2393,0.31261 -0.4863,0.61103 -0.5488,0.66313 -2.75,2.29064 -3.6562,7.31753 -2.0075,11.13601 0.8388,1.94262 2.4099,4.66651 2.975,5.15779 0.06,0.0521 0.4803,0.64892 0.9342,1.32626 2.5655,3.82826 5.1557,6.9409 7.4826,8.99167 0.9379,0.82653 2.1072,1.88607 2.5986,2.35453 0.4913,0.46846 1.0455,0.93331 1.2315,1.03299 0.2831,0.15176 2.2094,1.69445 2.5169,2.01576 1.765,1.84365 2.6901,2.35308 2.8085,1.54654 z"
id="path21270" /><path
style="display:inline;fill:#812973;fill-opacity:1;stroke-width:0.71608"
d="m -1542.7376,-871.23997 c 0.2862,-0.71515 0.3795,-1.94635 0.6059,-7.98636 0.3653,-9.74366 2.0798,-19.54708 4.4068,-25.19856 0.4076,-0.98996 1.1027,-2.73252 1.5447,-3.87237 1.2218,-3.15114 4.0398,-8.5793 6.3448,-12.2215 4.396,-6.94599 8.186,-12.56318 11.3827,-16.86998 1.3546,-1.82507 2.6392,-3.57623 2.8546,-3.89147 0.2155,-0.31523 0.5298,-0.74153 0.6984,-0.94732 0.1686,-0.20577 0.8479,-1.09883 1.5094,-1.98457 1.3806,-1.84865 3.2151,-4.21831 3.3734,-4.35765 0.059,-0.0521 0.5291,-0.64891 1.0442,-1.32625 0.515,-0.67732 1.5341,-1.90806 2.2646,-2.73496 0.7306,-0.82689 1.9252,-2.19642 2.6546,-3.04337 0.7295,-0.84697 1.7526,-1.99705 2.2736,-2.55575 0.521,-0.5587 1.4702,-1.57641 2.1094,-2.26158 0.6392,-0.68517 1.4791,-1.62942 1.8666,-2.09834 0.3874,-0.46893 1.4509,-1.66254 2.3633,-2.65249 0.9122,-0.98994 2.0126,-2.21084 2.4452,-2.7131 0.4325,-0.50226 1.8728,-1.97506 3.2005,-3.27287 3.4847,-3.40602 8.8565,-8.74429 10.2086,-10.14491 1.7687,-1.83199 3.8756,-3.82394 4.6153,-4.36366 0.3648,-0.26605 0.9616,-0.76408 1.3263,-1.10671 0.3648,-0.34263 1.132,-0.94417 1.7052,-1.33677 0.5731,-0.3926 1.2422,-0.9045 1.5455,-1.11477 0.282,-0.19555 0.6671,-0.43095 0.8166,-0.53453 0.135,-0.0936 0.4514,-0.21475 0.764,-0.42178 0.3127,-0.20703 0.4612,-0.17118 0.9836,-0.37768 2.2253,-0.87975 4.476,0.11177 6.1894,1.57586 0.3272,0.40939 1.064,1.27474 1.637,1.92299 0.5732,0.64828 1.4425,1.68744 1.9319,2.30928 1.2944,1.64486 2.1747,2.5513 3.5625,3.66878 0.6774,0.54536 1.6578,1.44392 2.179,1.99682 0.5767,0.6121 1.7993,1.51779 3.126,2.31576 1.1984,0.72078 2.5199,1.65154 2.9367,2.06837 0.4168,0.41683 1.2268,1.01548 1.7999,1.33036 1.8397,1.01067 7.0653,3.59861 8.3363,4.12849 4.1139,2.01877 8.5144,3.56127 12.9783,4.62318 0.521,0.0928 1.5868,0.31849 2.3683,0.50167 2.9593,0.69362 3.9531,-0.25782 1.5541,-1.48799 -0.2817,-0.14444 -1.0732,-0.56358 -1.759,-0.93143 -0.6857,-0.36784 -2.0072,-0.98636 -2.9366,-1.37447 -6.1224,-2.55672 -14.0957,-7.14843 -16.6574,-9.59277 -0.521,-0.49715 -1.2457,-1.14719 -1.6105,-1.44454 -2.1172,-1.7262 -5.7038,-5.35554 -8.5522,-8.65448 -0.3502,-0.40558 -1.2722,-1.41431 -2.0488,-2.24163 -1.7108,-1.82231 -2.0356,-2.65361 -2.2824,-3.01532 -1.2058,-1.76765 -1.7477,-2.63188 -2.5598,-4.92227 -1.559,-3.69329 -2.2226,-4.54719 -3.5978,-5.47479 -0.4462,-0.301 -1.1291,-0.257 -1.7301,-0.3396 -1.1835,-0.03 -2.6585,-0.083 -4.3961,0.6424 -4.2764,2.2443 -4.4754,2.3756 -7.0415,4.66753 -0.6589,0.58851 -1.421,1.51352 -1.8841,1.86936 -0.4632,0.35582 -1.1874,1.03063 -1.6094,1.49955 -0.422,0.46892 -2.171,2.21671 -3.8866,3.88398 -5.6083,5.44995 -8.9789,8.87071 -10.7135,10.8728 -0.9379,1.08242 -2.3446,2.62265 -3.1262,3.42272 -0.7815,0.80008 -2.1027,2.21644 -2.9358,3.14746 -0.8331,0.93103 -1.9415,2.12332 -2.463,2.64954 -0.5215,0.52622 -1.4145,1.50668 -1.9842,2.17882 -0.5699,0.67213 -1.4117,1.5631 -1.8708,1.97992 -1.9676,1.7866 -2.8699,2.74685 -4.0058,4.26292 -0.6637,0.88573 -1.3537,1.78095 -1.5334,1.98935 -0.1797,0.20841 -0.6603,0.7626 -1.0681,1.23151 -1.4238,1.63741 -5.0422,6.27716 -5.9271,7.60002 -0.1563,0.23368 -0.7132,0.9003 -1.2376,1.48137 -0.5244,0.58107 -1.334,1.56767 -1.7992,2.19246 -0.4652,0.62478 -1.6177,2.11644 -2.5612,3.3148 -1.6506,2.09676 -5.5239,7.81878 -8.0593,11.90625 -0.6686,1.07769 -1.546,2.48445 -1.9496,3.12613 -0.4039,0.64169 -1.3744,2.31768 -2.1568,3.72444 -0.7825,1.40676 -1.7935,3.1266 -2.2469,3.82186 -0.4534,0.69527 -0.9868,1.67649 -1.1854,2.18051 -0.1987,0.504 -0.536,1.25666 -0.7496,1.67255 -1.4969,2.91424 -3.2808,7.9129 -4.0226,11.27137 -0.1264,0.57312 -0.3049,1.34046 -0.3964,1.70516 -1.0738,4.28096 -1.0368,31.97216 0.046,34.34866 0.324,0.71124 0.6487,0.64037 0.9915,-0.21641 z"
id="path21268" /><path
style="display:inline;fill:#812973;fill-opacity:1;stroke-width:0.71608"
d="m -1078.2395,-870.79523 c 0.7098,-7.13011 0.2636,-31.53813 -0.6564,-35.89493 -0.093,-0.44397 -2.6652,-7.38571 -5.8012,-13.55489 -2.6861,-5.28448 -3.3397,-6.45631 -4.5271,-8.11617 -1.2048,-1.68457 2.9545,4.03021 -4.8475,-7.74839 -1.1226,-1.69496 -2.3903,-3.50803 -2.817,-4.02904 -0.4268,-0.52102 -1.0546,-1.33099 -1.3953,-1.79991 -0.3407,-0.46892 -1.1011,-1.49202 -1.6897,-2.27354 -0.5886,-0.78154 -1.5187,-2.01778 -2.0669,-2.74722 -0.5481,-0.72944 -1.2546,-1.66727 -1.5699,-2.0841 -0.5298,-0.70052 -3.2613,-4.41584 -4.6346,-6.30389 -3.2216,-4.42897 -12.6092,-16.84587 -22.6349,-26.9449 -3.4964,-3.52219 -6.2044,-6.37773 -8.264,-8.71446 -1.0968,-1.24432 -2.8294,-2.94055 -4.5507,-4.45515 -1.8669,-1.64264 -2.084,-1.86732 -3.8454,-3.97872 -1.7014,-2.03966 -2.5666,-2.88156 -5.0597,-3.70856 -0.7207,-0.2391 -4.0827,-0.3408 -4.9339,-0.1491 -2.2674,0.5104 -3.5159,1.4734 -4.387,3.38401 -0.8915,1.9552 -0.9137,2.0117 -1.37,3.47936 -1.7346,3.28744 -4.1996,6.62796 -6.8429,9.33273 -0.7561,0.71659 -1.688,1.6013 -2.0708,1.96601 -0.3829,0.36471 -1.1544,1.04678 -1.7145,1.5157 -0.56,0.46892 -1.4063,1.2077 -1.8805,1.64173 -1.6622,1.52141 -3.0268,2.49602 -10.5875,7.56197 -3.0309,2.03077 -3.4789,2.31622 -4.3423,2.76676 -0.03,0.0156 -1.8058,1.23781 -4.1876,2.32977 -1.1149,0.59434 -2.9536,1.42519 -3.1541,1.42519 -0.084,0 -0.6941,0.25578 -1.3557,0.56838 -0.6617,0.31262 -1.275,0.5684 -1.3629,0.5684 -0.088,0 -0.4074,0.11936 -0.71,0.26524 -0.3026,0.14589 -1.0463,0.49593 -1.6527,0.77785 -1.1309,0.52574 -1.6101,1.12184 -1.3813,1.71808 0.2476,0.64512 3.4959,0.23053 7.8836,-1.00616 0.8857,-0.24965 1.9088,-0.5279 2.2735,-0.61833 1.4779,-0.3664 5.6207,-1.88788 7.768,-2.85287 4.4301,-1.99081 12.5209,-6.68979 15.0747,-8.75506 0.8508,-0.68804 2.5045,-1.97868 3.2086,-2.50409 1.2827,-0.95731 5.108,-4.65017 7.7679,-7.49895 3.5334,-3.78443 6.7745,-4.49663 9.3768,-2.06043 0.3639,0.34064 1.151,0.97546 1.7491,1.4107 1.4877,1.08252 5.5623,4.79321 7.6241,6.94318 2.8062,2.9263 6.459,6.712 9.6693,10.02119 4.595,4.73639 7.9926,8.59798 12.2446,13.91698 0.5831,0.72944 1.8606,2.22829 2.839,3.33078 0.9783,1.10249 3.0575,3.56479 4.6207,5.47176 1.563,1.90699 3.3702,4.1086 4.0161,4.89249 1.168,1.41764 5.5534,7.3874 6.2239,8.47213 0.1931,0.31262 1.5064,2.3035 2.9182,4.42418 1.4119,2.12069 2.8147,4.25214 3.1173,4.73658 0.3027,0.48443 0.9102,1.43496 1.3503,2.11228 1.6435,2.53022 2.9389,4.71285 4.2169,7.10487 0.7238,1.35466 1.5413,2.84667 1.8168,3.31559 1.0481,1.78405 1.8025,3.38892 2.5828,5.49443 0.4441,1.19835 0.9748,2.60511 1.1792,3.12614 1.6288,4.15067 2.5061,7.60396 3.297,12.9782 0.2684,1.82358 0.5194,3.52875 0.5578,3.78926 0.2807,1.90278 0.6632,6.91398 0.8317,10.89412 0.2169,5.1239 0.4391,7.07044 0.9083,7.95744 0.6754,1.27705 0.8975,0.91971 1.1778,-1.89462 z"
id="path21266" /><path
style="display:inline;fill:#812973;fill-opacity:1;stroke-width:0.71608"
d="m -1427.4952,-780.23842 c -0.066,-0.26406 -0.6617,-1.39373 -1.3231,-2.51039 -2.653,-4.47862 -6.2867,-11.44353 -7.6191,-14.60376 -0.3793,-0.89993 -1.0283,-2.40098 -1.4418,-3.33566 -0.9896,-2.23618 -2.1411,-5.22374 -3.3612,-8.72103 -0.5453,-1.56307 -1.1456,-3.2256 -1.3337,-3.69452 -0.5822,-1.45044 -1.2685,-3.60656 -2.9242,-9.18688 -0.4944,-1.66612 -0.9747,-3.17098 -1.0673,-3.34412 -0.4258,-0.79558 -1.5016,-4.0549 -1.5016,-4.54935 0,-0.30354 -0.1113,-0.82623 -0.2474,-1.16155 -0.136,-0.33532 -0.4006,-1.29173 -0.588,-2.12537 -0.1874,-0.83363 -0.4881,-2.06988 -0.6685,-2.74722 -0.7629,-2.86666 -1.3739,-5.26023 -1.5234,-5.96807 -0.088,-0.41681 -0.3295,-1.39729 -0.5363,-2.17882 -0.4471,-1.68801 -1.4363,-6.56762 -2.0164,-9.94681 -0.1431,-0.83363 -0.444,-2.32565 -0.6687,-3.3156 -0.2247,-0.98994 -0.5297,-2.3932 -0.6778,-3.11834 -0.148,-0.72516 -0.394,-1.74826 -0.5467,-2.27356 -0.1526,-0.52531 -0.4076,-1.93557 -0.5668,-3.13393 -0.1591,-1.19834 -0.4188,-2.90351 -0.5772,-3.78926 -0.2878,-1.61166 -0.744,-5.01553 -1.4343,-10.70466 -0.2022,-1.66727 -0.4921,-3.79872 -0.6441,-4.73657 -0.585,-3.60923 -1.3977,-10.37009 -1.6169,-13.45186 -0.6689,-9.40111 1.4723,-13.19364 7.7654,-13.75441 2.1828,-0.1945 5.4548,-0.76039 9.7573,-1.6874 0.9379,-0.20208 2.2594,-0.4594 2.9367,-0.57184 2.6892,-0.44638 6.2309,-1.16444 7.5785,-1.5365 2.4255,-0.66961 2.6028,-0.73581 6.6313,-2.47614 1.8571,-0.8023 2.9351,-1.4068 5.9207,-3.31997 1.6016,-1.02635 2.6472,-2.51542 1.7661,-2.51542 -0.348,0 -2.9272,0.69728 -6.1711,1.66843 -1.4068,0.42115 -2.9415,0.82206 -3.4103,0.89092 -0.4689,0.0689 -1.3216,0.24216 -1.8947,0.38513 -0.9428,0.23514 -2.3933,0.47904 -7.9575,1.33787 -10.0538,1.55184 -18.2839,1.72407 -26.1458,0.54715 -6.6202,-0.99103 -8.5693,0.97199 -7.6202,7.67506 0.1253,0.88573 0.3115,3.01719 0.4135,4.73656 0.3952,6.65485 1.0277,14.16501 1.4201,16.86219 0.5264,3.61822 1.2554,8.01942 1.355,8.18059 0.046,0.0736 0.2948,1.35375 0.554,2.8447 0.2591,1.49093 0.6862,3.86177 0.9489,5.26853 0.2627,1.40676 0.6054,3.32507 0.7616,4.26291 0.2589,1.55653 0.579,3.09324 1.598,7.67325 0.197,0.88574 0.4557,2.16461 0.5748,2.84195 0.1191,0.67732 0.2962,1.44546 0.3937,1.70696 0.1761,0.47274 0.7343,2.93123 1.5695,6.9136 0.2404,1.14625 0.6531,2.76615 0.9173,3.59979 0.2641,0.83364 0.6689,2.19777 0.8996,3.03141 0.2308,0.83364 0.5702,1.85676 0.7545,2.27362 0.1841,0.41686 0.3991,1.0563 0.4778,1.42097 0.079,0.36468 0.2641,0.89997 0.412,1.18952 0.148,0.28955 0.6399,1.95209 1.0933,3.69452 0.4533,1.74244 1.054,3.76488 1.3348,4.49431 0.281,0.72943 0.8937,2.47723 1.3617,3.88399 1.0628,3.19377 3.3308,9.04569 4.0485,10.44531 0.087,0.16998 0.5568,1.27834 1.0435,2.46303 0.4868,1.18467 0.9471,2.23922 1.0228,2.34342 0.076,0.1042 0.2529,0.48786 0.3939,0.85258 0.8084,2.09342 3.8715,8.00474 5.4128,10.44628 0.2514,0.39847 0.4573,0.77535 0.4573,0.83747 0,0.0622 0.4065,0.7246 0.9033,1.47211 0.4969,0.74753 1.008,1.57228 1.1358,1.83278 0.1277,0.26052 0.6705,1.15573 1.206,1.98937 0.5357,0.83364 1.1902,1.89936 1.4546,2.36828 0.7343,1.30211 5.185,7.96679 6.3853,9.56157 1.6458,2.18686 3.7314,3.6869 3.4242,2.46288 z"
id="path21264" /><path
style="display:inline;fill:#812973;fill-opacity:1;stroke-width:0.71608"
d="m -1194.7853,-780.1521 c 2.1923,-2.10664 6.1213,-7.42853 8.2772,-11.21191 0.9164,-1.6081 1.2324,-2.11477 3.3378,-5.35124 3.1986,-4.91726 4.0506,-6.37827 6.2424,-10.70465 1.2935,-2.55301 2.5372,-5.06813 2.7642,-5.58915 0.2267,-0.52102 0.6849,-1.58675 1.0181,-2.36829 0.3332,-0.78154 0.7553,-1.76201 0.938,-2.17883 1.3324,-3.0396 2.8522,-6.98255 4.5276,-11.74669 1.0884,-3.09505 2.9791,-9.18527 3.1469,-10.13627 0.065,-0.36471 0.2725,-1.17467 0.4626,-1.79989 0.4298,-1.41305 1.4193,-4.70962 1.905,-6.34702 0.2009,-0.67732 0.4933,-1.78568 0.6497,-2.46301 0.1564,-0.67733 0.5031,-2.12672 0.7703,-3.22087 0.6395,-2.61793 2.0454,-8.97072 2.8511,-12.88348 0.354,-1.71938 0.7344,-3.42453 0.8452,-3.78926 0.4172,-1.37282 2.0398,-10.61296 2.1958,-12.50455 0.056,-0.67733 0.3076,-2.38249 0.5593,-3.78925 0.2515,-1.40677 0.5434,-3.36771 0.6485,-4.35766 0.1049,-0.98994 0.3567,-2.78036 0.5593,-3.97872 3.8712,-22.89397 2.8792,-26.41354 -6.9973,-24.82731 -6.2063,0.99676 -20.7722,0.69409 -27.3774,-0.56888 -1.0197,-0.19496 -3.9937,-0.74938 -6.4418,-1.20083 -0.8336,-0.15375 -2.6666,-0.45883 -4.0733,-0.67797 -1.4069,-0.21915 -2.8136,-0.47608 -3.1262,-0.57097 -0.6394,-0.19409 -3.6538,-0.86857 -4.593,-1.0277 -0.5731,-0.0971 -0.5986,-0.079 -0.3807,0.26981 0.1284,0.20564 0.2862,0.37389 0.3507,0.37389 1.2035,0 2.0655,3.24173 19.3064,8.15076 4.2795,1.20844 9.0922,1.94859 15.3141,2.35516 3.6958,0.2415 5.2294,0.8093 6.5163,2.41246 2.4232,3.01901 2.6974,7.10045 1.3091,19.47976 -0.2045,1.82358 -0.4691,4.38133 -0.588,5.68389 -0.455,4.98503 -1.3988,11.55273 -2.287,15.91489 -0.3077,1.51097 -0.8137,4.0261 -1.1245,5.58916 -0.5784,2.90983 -1.8205,8.92228 -2.368,11.4625 -0.1684,0.78154 -0.3837,1.87182 -0.4782,2.42286 -0.221,1.28722 -0.9407,4.61225 -1.0302,4.75902 -0.038,0.0618 -0.3814,1.64709 -0.7638,3.52278 -0.8278,4.06011 -1.1576,5.29252 -3.4222,12.78873 -0.1417,0.46894 -0.5258,1.74781 -0.8534,2.84195 -0.3277,1.09416 -0.7524,2.45829 -0.9438,3.03141 -0.1912,0.57312 -0.6471,1.97988 -1.0131,3.12614 -0.366,1.14625 -1.1018,3.27771 -1.6353,4.73657 -0.5334,1.45886 -1.3009,3.63295 -1.7055,4.83131 -2.2837,6.76422 -3.7336,10.3713 -5.854,14.56423 -1.6135,3.19059 -1.4241,2.87254 -4.4717,7.5082 -0.959,1.45886 -2.0711,3.20665 -2.471,3.88399 -0.4,0.67732 -0.7751,1.27413 -0.8336,1.32623 -0.1404,0.12514 -0.7091,1.08138 -1.0883,1.83004 -0.673,1.32864 0.2121,1.59478 1.4257,0.42866 z"
id="path21262" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1273.7037,-977.54923 c -0.772,-0.47069 -1.0237,-0.8741 -1.7784,-2.85028 -2.3054,-6.03708 -11.9745,-9.16172 -18.1556,-5.8672 -2.0578,1.09679 -4.9207,4.85312 -5.3221,6.01429 -0.6057,1.75171 -1.9555,3.17543 -3.3116,1.81937 -0.529,-0.52902 -0.021,-3.8741 0.9303,-6.13066 5.9705,-14.15649 23.3562,-13.47962 29.0968,1.13279 0.4222,1.0744 1.1272,4.11765 1.1325,4.88772 0.01,1.10354 -1.4779,1.67321 -2.5919,0.99397 z"
id="path21275" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1354.8435,-977.87889 c -1.6707,-1.43007 -0.6421,-5.87088 2.361,-10.19386 7.0117,-10.09294 18.5658,-9.86933 25.6798,0.497 2.0724,3.01978 3.6886,8.04135 2.877,8.93823 -1.0773,1.19052 -2.6788,0.56325 -3.0747,-0.28432 -0.5216,-1.11631 -2.4548,-4.72705 -4.3919,-6.25286 -7.586,-5.97524 -17.0614,-3.29825 -20.7104,5.85112 -0.6548,1.64176 -1.0982,2.14494 -1.8903,2.14494 -0.017,0 -0.4005,-0.31511 -0.8505,-0.70025 z"
id="path21274" /><path
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.71608"
d="m -1314.6853,-934.56573 c 0,0.0665 -3.1068,-0.30747 -4.912,-0.52467 -0.1929,-0.0232 -0.9132,-0.13398 -1.6947,-0.29027 -1.2597,-0.25194 -1.3953,-0.2116 -3.1507,-0.83734 -0.3125,-0.11144 -1.421,-0.47049 -2.463,-0.7979 -9.4379,-2.96537 -18.6541,-11.35821 -21.1051,-19.21954 -1.2012,-3.85268 -0.8806,-4.07451 3.5747,-2.47362 0.7788,0.27982 2.1003,0.58344 2.9366,0.67473 0.8365,0.0913 1.8193,0.25162 2.184,0.3563 0.3647,0.10469 1.6436,0.31785 2.8419,0.47371 1.1984,0.15586 2.7757,0.40839 3.5052,0.56117 0.7294,0.15278 2.3283,0.37049 3.5529,0.48379 1.2247,0.1133 2.8445,0.33866 3.5997,0.5008 0.7552,0.16213 2.0552,0.32707 2.8888,0.36656 0.8338,0.0394 2.283,0.12906 3.2209,0.19907 4.655,0.34753 16.2868,0.0317 20.9357,-0.56822 1.1983,-0.15468 2.7329,-0.28504 3.4103,-0.28969 0.6773,-0.005 1.9562,-0.13871 2.8419,-0.29788 0.8858,-0.15919 3.0598,-0.53911 4.8314,-0.84429 2.3425,-0.40355 3.7375,-0.75702 5.1155,-1.29615 2.3947,-0.93695 2.9873,-1.06659 3.5413,-0.77474 2.9049,1.53048 -3.8173,13.28525 -9.9576,17.412 -0.5071,0.34075 -1.4846,1.04554 -2.1725,1.56619 -1.9371,1.46617 -5.0104,2.90117 -7.7997,3.64196 -0.6774,0.17988 -1.5749,0.44618 -1.9945,0.59178 -0.4195,0.1456 -1.6558,0.44242 -2.7472,0.6596 -1.7204,0.34234 -2.8441,0.46959 -6.9103,0.78248 -0.4169,0.0321 -2.2499,0.007 -4.0735,-0.0558 z"
id="path21273" /><path
id="path21276-14"
style="display:inline;fill:#6d2361;fill-opacity:1;stroke-width:0.720242"
d="m -1245.0685,-1055.6205 a 86.1772,81.849525 0 0 1 -25.2215,20.4684 c 1.3705,1.0078 2.5102,1.8362 3.7749,2.8552 0.5487,0.4422 1.3363,1.1592 1.8065,1.6 0.4702,0.441 1.0743,0.9732 1.9413,1.6682 1.6057,1.2868 2.1849,1.7488 2.7438,2.126 1.2474,0.8413 1.6524,1.6882 2.4775,0.8079 v 0 c 0.6031,-0.6437 0.9274,-1.0717 1.8354,-2.4244 0.8999,-1.3407 4.7148,-8.3481 5.1155,-9.3968 0.141,-0.3689 0.319,-0.7571 0.3957,-0.8625 0.077,-0.1054 0.447,-1.0111 0.8233,-2.0126 0.3763,-1.0014 0.8464,-2.252 1.0446,-2.7792 1.3959,-3.7118 2.4649,-7.8224 3.263,-12.0539 z m -133.0717,2.3338 c 0.8022,4.0023 1.9581,8.1841 2.9725,10.4828 0.2326,0.527 0.526,1.3033 0.652,1.725 0.3682,1.2319 0.7939,2.2039 1.6375,3.7375 0.4348,0.7906 0.9873,1.9318 1.2278,2.5356 0.2404,0.6038 0.9998,1.8114 1.6878,2.6833 1.5618,1.9797 2.3012,3.3632 2.9503,4.1981 1.6195,2.0828 1.8125,2.3532 4.1571,-0.086 1.0457,-1.0876 2.7113,-2.6246 3.7012,-3.4157 0.99,-0.791 1.9279,-1.5619 2.0841,-1.713 0.4811,-0.4653 1.4143,-1.1458 2.534,-1.8984 a 86.1772,81.849525 0 0 1 -23.6043,-18.2495 z" /><path
id="path21276-1"
style="display:inline;fill:#985289;fill-opacity:1;stroke-width:0.71608"
d="m -1311.1821,-1147.2592 c -3.2889,3e-4 -6.5629,0.2086 -8.2398,0.6244 -0.5732,0.1427 -1.4257,0.352 -1.8946,0.4651 -1.9029,0.459 -5.2315,1.3853 -5.5514,1.5449 -0.1878,0.093 -0.9749,0.336 -1.7488,0.5388 -0.7741,0.2026 -1.7713,0.5687 -2.2163,0.8131 -0.445,0.2442 -1.421,0.6748 -2.1688,0.9568 -0.7479,0.2821 -1.4149,0.7081 -1.5986,0.8004 -1.0377,0.5217 -2.6866,1.1878 -3.5783,1.7038 -0.469,0.2712 -1.5773,0.8797 -2.4631,1.3521 -0.8857,0.4724 -1.7382,0.9637 -1.8946,1.092 -0.1563,0.1282 -1.3925,0.9408 -2.7473,1.8058 -2.5194,1.6087 -4.4043,3.0459 -6.909,5.2684 -0.7789,0.6911 -1.8449,1.6336 -2.369,2.0943 -1.4987,1.3175 -5.6597,5.6042 -7.4852,7.7114 -3.0994,3.5774 -6.0728,8.0025 -8.1759,12.0368 0.5837,-0.6509 1.1617,-1.3213 1.7548,-1.9282 2.0589,-2.1073 6.752,-6.394 8.4421,-7.7114 0.5912,-0.4607 1.7935,-1.4034 2.6718,-2.0945 2.8249,-2.2223 4.9508,-3.6595 7.7924,-5.2683 1.5279,-0.865 2.9218,-1.6777 3.098,-1.8058 0.1763,-0.1282 1.1381,-0.6197 2.1371,-1.092 0.9989,-0.4725 2.249,-1.0808 2.7779,-1.3522 1.0056,-0.516 2.865,-1.182 4.0353,-1.7036 0.2072,-0.092 0.9593,-0.5183 1.8028,-0.8004 0.8435,-0.2821 1.9442,-0.7128 2.446,-0.957 0.5019,-0.2443 1.6268,-0.6102 2.4997,-0.813 0.8729,-0.2027 1.7606,-0.4451 1.9724,-0.5388 0.3607,-0.1596 4.115,-1.0859 6.2611,-1.5449 0.5289,-0.1131 1.4902,-0.3225 2.1367,-0.4651 3.7823,-0.8314 14.7699,-0.8337 18.6867,0 5.0249,1.125 9.8346,2.5794 14.4992,4.5533 11.4814,4.1533 22.6764,11.7898 33.1545,22.6161 0.3957,0.4087 0.7633,0.8274 1.1441,1.2415 -2.0368,-3.9403 -4.5999,-7.7496 -7.6894,-11.3498 -9.2906,-10.8261 -19.2173,-18.463 -29.3975,-22.6163 -4.1359,-1.9739 -8.4004,-3.4283 -12.8557,-4.5533 -1.7365,-0.4169 -5.0404,-0.6246 -8.3293,-0.6244 z" /></g></g></svg>

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -11,6 +11,7 @@ from typing import Dict
class TemplateNotFoundError(Exception):
"""Raised when a template file cannot be found."""
pass
@@ -46,7 +47,7 @@ def load_template(name: str, **kwargs) -> str:
# Check cache first
if name not in _template_cache:
# Determine file path based on whether name has an extension
if '.' in name:
if "." in name:
file_path = _TEMPLATE_DIR / name
else:
file_path = _TEMPLATE_DIR / f"{name}.html"
@@ -54,7 +55,7 @@ def load_template(name: str, **kwargs) -> str:
if not file_path.exists():
raise TemplateNotFoundError(f"Template '{name}' not found at {file_path}")
_template_cache[name] = file_path.read_text(encoding='utf-8')
_template_cache[name] = file_path.read_text(encoding="utf-8")
template = _template_cache[name]

View File

@@ -1,25 +1,76 @@
#!/usr/bin/env python3
from typing import Dict, List, Tuple
from typing import Dict, List, Tuple, Optional
from collections import defaultdict
from datetime import datetime
from zoneinfo import ZoneInfo
import re
import urllib.parse
from wordlists import get_wordlists
from database import get_database, DatabaseManager
from ip_utils import is_local_or_private_ip, is_valid_public_ip
class AccessTracker:
"""Track IP addresses and paths accessed"""
def __init__(self):
"""
Track IP addresses and paths accessed.
Maintains in-memory structures for fast dashboard access and
persists data to SQLite for long-term storage and analysis.
"""
def __init__(
self,
max_pages_limit,
ban_duration_seconds,
db_manager: Optional[DatabaseManager] = None,
):
"""
Initialize the access tracker.
Args:
db_manager: Optional DatabaseManager for persistence.
If None, will use the global singleton.
"""
self.max_pages_limit = max_pages_limit
self.ban_duration_seconds = ban_duration_seconds
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.credential_attempts: List[Dict] = []
# Memory limits for in-memory lists (prevents unbounded growth)
self.max_access_log_size = 10_000 # Keep only recent 10k accesses
self.max_credential_log_size = 5_000 # Keep only recent 5k attempts
self.max_counter_keys = 100_000 # Max unique IPs/paths/user agents
# Track pages visited by each IP (for good crawler limiting)
self.ip_page_visits: Dict[str, Dict[str, object]] = defaultdict(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'
"bot",
"crawler",
"spider",
"scraper",
"curl",
"wget",
"python-requests",
"scanner",
"nikto",
"sqlmap",
"nmap",
"masscan",
"nessus",
"acunetix",
"burp",
"zap",
"w3af",
"metasploit",
"nuclei",
"gobuster",
"dirbuster",
]
# Load attack patterns from wordlists
@@ -29,16 +80,35 @@ class AccessTracker:
# Fallback if wordlists not loaded
if not self.attack_types:
self.attack_types = {
'path_traversal': r'\.\.',
'sql_injection': r"('|--|;|\bOR\b|\bUNION\b|\bSELECT\b|\bDROP\b)",
'xss_attempt': r'(<script|javascript:|onerror=|onload=)',
'common_probes': r'(wp-admin|phpmyadmin|\.env|\.git|/admin|/config)',
'shell_injection': r'(\||;|`|\$\(|&&)',
"path_traversal": r"\.\.",
"sql_injection": r"('|--|;|\bOR\b|\bUNION\b|\bSELECT\b|\bDROP\b)",
"xss_attempt": r"(<script|javascript:|onerror=|onload=)",
"common_probes": r"(wp-admin|phpmyadmin|\.env|\.git|/admin|/config)",
"shell_injection": r"(\||;|`|\$\(|&&)",
}
# Track IPs that accessed honeypot paths from robots.txt
self.honeypot_triggered: Dict[str, List[str]] = defaultdict(list)
# Database manager for persistence (lazily initialized)
self._db_manager = db_manager
@property
def db(self) -> Optional[DatabaseManager]:
"""
Get the database manager, lazily initializing if needed.
Returns:
DatabaseManager instance or None if not available
"""
if self._db_manager is None:
try:
self._db_manager = get_database()
except Exception:
# Database not initialized, persistence disabled
pass
return self._db_manager
def parse_credentials(self, post_data: str) -> Tuple[str, str]:
"""
Parse username and password from POST data.
@@ -55,14 +125,22 @@ class AccessTracker:
parsed = urllib.parse.parse_qs(post_data)
# Common username field names
username_fields = ['username', 'user', 'login', 'email', 'log', 'userid', 'account']
username_fields = [
"username",
"user",
"login",
"email",
"log",
"userid",
"account",
]
for field in username_fields:
if field in parsed and parsed[field]:
username = parsed[field][0]
break
# Common password field names
password_fields = ['password', 'pass', 'passwd', 'pwd', 'passphrase']
password_fields = ["password", "pass", "passwd", "pwd", "passphrase"]
for field in password_fields:
if field in parsed and parsed[field]:
password = parsed[field][0]
@@ -70,8 +148,12 @@ class AccessTracker:
except Exception:
# If parsing fails, try simple regex patterns
username_match = re.search(r'(?:username|user|login|email|log)=([^&\s]+)', post_data, re.IGNORECASE)
password_match = re.search(r'(?:password|pass|passwd|pwd)=([^&\s]+)', post_data, re.IGNORECASE)
username_match = re.search(
r"(?:username|user|login|email|log)=([^&\s]+)", post_data, re.IGNORECASE
)
password_match = re.search(
r"(?:password|pass|passwd|pwd)=([^&\s]+)", post_data, re.IGNORECASE
)
if username_match:
username = urllib.parse.unquote_plus(username_match.group(1))
@@ -80,48 +162,136 @@ class AccessTracker:
return username, password
def record_credential_attempt(self, ip: str, path: str, username: str, password: str):
"""Record a credential login attempt"""
self.credential_attempts.append({
'ip': ip,
'path': path,
'username': username,
'password': password,
'timestamp': datetime.now().isoformat()
})
def record_credential_attempt(
self, ip: str, path: str, username: str, password: str
):
"""
Record a credential login attempt.
Stores in both in-memory list and SQLite database.
Skips recording if the IP is the server's own public IP.
"""
# Skip if this is the server's own IP
from config import get_config
config = get_config()
server_ip = config.get_server_ip()
if server_ip and ip == server_ip:
return
# In-memory storage for dashboard
self.credential_attempts.append(
{
"ip": ip,
"path": path,
"username": username,
"password": password,
"timestamp": datetime.now().isoformat(),
}
)
# Trim if exceeding max size (prevent unbounded growth)
if len(self.credential_attempts) > self.max_credential_log_size:
self.credential_attempts = self.credential_attempts[
-self.max_credential_log_size :
]
# Persist to database
if self.db:
try:
self.db.persist_credential(
ip=ip, path=path, username=username, password=password
)
except Exception:
# Don't crash if database persistence fails
pass
def record_access(
self,
ip: str,
path: str,
user_agent: str = "",
body: str = "",
method: str = "GET",
):
"""
Record an access attempt.
Stores in both in-memory structures and SQLite database.
Skips recording if the IP is the server's own public IP.
Args:
ip: Client IP address
path: Requested path
user_agent: Client user agent string
body: Request body (for POST/PUT)
method: HTTP method
"""
# Skip if this is the server's own IP
from config import get_config
config = get_config()
server_ip = config.get_server_ip()
if server_ip and ip == server_ip:
return
def record_access(self, ip: str, path: str, user_agent: str = '', body: 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
# path attack type detection
# Path attack type detection
attack_findings = self.detect_attack_type(path)
# post / put data
# POST/PUT body attack detection
if len(body) > 0:
attack_findings.extend(self.detect_attack_type(body))
is_suspicious = self.is_suspicious_user_agent(user_agent) or self.is_honeypot_path(path) or len(attack_findings) > 0
is_suspicious = (
self.is_suspicious_user_agent(user_agent)
or self.is_honeypot_path(path)
or len(attack_findings) > 0
)
is_honeypot = self.is_honeypot_path(path)
# Track if this IP accessed a honeypot path
if self.is_honeypot_path(path):
if is_honeypot:
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),
'attack_types':attack_findings,
'timestamp': datetime.now().isoformat()
})
# In-memory storage for dashboard
self.access_log.append(
{
"ip": ip,
"path": path,
"user_agent": user_agent,
"suspicious": is_suspicious,
"honeypot_triggered": self.is_honeypot_path(path),
"attack_types": attack_findings,
"timestamp": datetime.now().isoformat(),
}
)
def detect_attack_type(self, data:str) -> list[str]:
# Trim if exceeding max size (prevent unbounded growth)
if len(self.access_log) > self.max_access_log_size:
self.access_log = self.access_log[-self.max_access_log_size :]
# Persist to database
if self.db:
try:
self.db.persist_access(
ip=ip,
path=path,
user_agent=user_agent,
method=method,
is_suspicious=is_suspicious,
is_honeypot_trigger=is_honeypot,
attack_types=attack_findings if attack_findings else None,
)
except Exception:
# Don't crash if database persistence fails
pass
def detect_attack_type(self, data: str) -> list[str]:
"""
Returns a list of all attack types found in path data
"""
@@ -134,27 +304,37 @@ class AccessTracker:
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/'
"/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'])
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"""
@@ -163,48 +343,340 @@ class AccessTracker:
ua_lower = user_agent.lower()
return any(pattern in ua_lower for pattern in self.suspicious_patterns)
def get_category_by_ip(self, client_ip: str) -> str:
"""
Check if an IP has been categorized as a 'good crawler' in the database.
Uses the IP category from IpStats table.
Args:
client_ip: The client IP address (will be sanitized)
Returns:
True if the IP is categorized as 'good crawler', False otherwise
"""
try:
from sanitizer import sanitize_ip
# Sanitize the IP address
safe_ip = sanitize_ip(client_ip)
# Query the database for this IP's category
db = self.db
if not db:
return False
ip_stats = db.get_ip_stats_by_ip(safe_ip)
if not ip_stats or not ip_stats.get("category"):
return False
# Check if category matches "good crawler"
category = ip_stats.get("category", "").lower().strip()
return category
except Exception as e:
# Log but don't crash on database errors
import logging
logging.error(f"Error checking IP category for {client_ip}: {str(e)}")
return False
def increment_page_visit(self, client_ip: str) -> int:
"""
Increment page visit counter for an IP and return the new count.
Implements incremental bans: each violation increases ban duration exponentially.
Ban duration formula: base_duration * (2 ^ violation_count)
- 1st violation: base_duration (e.g., 60 seconds)
- 2nd violation: base_duration * 2 (120 seconds)
- 3rd violation: base_duration * 4 (240 seconds)
- Nth violation: base_duration * 2^(N-1)
Args:
client_ip: The client IP address
Returns:
The updated page visit count for this IP
"""
# Skip if this is the server's own IP
from config import get_config
config = get_config()
server_ip = config.get_server_ip()
if server_ip and client_ip == server_ip:
return 0
try:
# Initialize if not exists
if client_ip not in self.ip_page_visits:
self.ip_page_visits[client_ip] = {
"count": 0,
"ban_timestamp": None,
"total_violations": 0,
"ban_multiplier": 1,
}
# Increment count
self.ip_page_visits[client_ip]["count"] += 1
# Set ban if reached limit
if self.ip_page_visits[client_ip]["count"] >= self.max_pages_limit:
# Increment violation counter
self.ip_page_visits[client_ip]["total_violations"] += 1
violations = self.ip_page_visits[client_ip]["total_violations"]
# Calculate exponential ban multiplier: 2^(violations - 1)
# Violation 1: 2^0 = 1x
# Violation 2: 2^1 = 2x
# Violation 3: 2^2 = 4x
# Violation 4: 2^3 = 8x, etc.
self.ip_page_visits[client_ip]["ban_multiplier"] = 2 ** (violations - 1)
# Set ban timestamp
self.ip_page_visits[client_ip][
"ban_timestamp"
] = datetime.now().isoformat()
return self.ip_page_visits[client_ip]["count"]
except Exception:
return 0
def is_banned_ip(self, client_ip: str) -> bool:
"""
Check if an IP is currently banned due to exceeding page visit limits.
Uses incremental ban duration based on violation count.
Ban duration = base_duration * (2 ^ (violations - 1))
Each time an IP is banned again, duration doubles.
Args:
client_ip: The client IP address
Returns:
True if the IP is banned, False otherwise
"""
try:
if client_ip in self.ip_page_visits:
ban_timestamp = self.ip_page_visits[client_ip].get("ban_timestamp")
if ban_timestamp is not None:
# Get the ban multiplier for this violation
ban_multiplier = self.ip_page_visits[client_ip].get(
"ban_multiplier", 1
)
# Calculate effective ban duration based on violations
effective_ban_duration = self.ban_duration_seconds * ban_multiplier
# Check if ban period has expired
ban_time = datetime.fromisoformat(ban_timestamp)
time_diff = datetime.now() - ban_time
if time_diff.total_seconds() > effective_ban_duration:
# Ban expired, reset for next cycle
# Keep violation count for next offense
self.ip_page_visits[client_ip]["count"] = 0
self.ip_page_visits[client_ip]["ban_timestamp"] = None
return False
else:
# Still banned
return True
return False
except Exception:
return False
def get_ban_info(self, client_ip: str) -> dict:
"""
Get detailed ban information for an IP.
Returns:
Dictionary with ban status, violations, and remaining ban time
"""
try:
if client_ip not in self.ip_page_visits:
return {
"is_banned": False,
"violations": 0,
"ban_multiplier": 1,
"remaining_ban_seconds": 0,
}
ip_data = self.ip_page_visits[client_ip]
ban_timestamp = ip_data.get("ban_timestamp")
if ban_timestamp is None:
return {
"is_banned": False,
"violations": ip_data.get("total_violations", 0),
"ban_multiplier": ip_data.get("ban_multiplier", 1),
"remaining_ban_seconds": 0,
}
# Ban is active, calculate remaining time
ban_multiplier = ip_data.get("ban_multiplier", 1)
effective_ban_duration = self.ban_duration_seconds * ban_multiplier
ban_time = datetime.fromisoformat(ban_timestamp)
time_diff = datetime.now() - ban_time
remaining_seconds = max(
0, effective_ban_duration - time_diff.total_seconds()
)
return {
"is_banned": remaining_seconds > 0,
"violations": ip_data.get("total_violations", 0),
"ban_multiplier": ban_multiplier,
"effective_ban_duration_seconds": effective_ban_duration,
"remaining_ban_seconds": remaining_seconds,
}
except Exception:
return {
"is_banned": False,
"violations": 0,
"ban_multiplier": 1,
"remaining_ban_seconds": 0,
}
"""
Get the current page visit count for an IP.
Args:
client_ip: The client IP address
Returns:
The page visit count for this IP
"""
try:
return self.ip_page_visits.get(client_ip, 0)
except Exception:
return 0
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]
"""Get top N IP addresses by access count (excludes local/private IPs)"""
filtered = [
(ip, count)
for ip, count in self.ip_counts.items()
if not is_local_or_private_ip(ip)
]
return sorted(filtered, 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]
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]
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)]
"""Get recent suspicious accesses (excludes local/private IPs)"""
suspicious = [
log
for log in self.access_log
if log.get("suspicious", False)
and not is_local_or_private_ip(log.get("ip", ""))
]
return suspicious[-limit:]
def get_attack_type_accesses(self, limit: int = 20) -> List[Dict]:
"""Get recent accesses with detected attack types"""
attacks = [log for log in self.access_log if log.get('attack_types')]
"""Get recent accesses with detected attack types (excludes local/private IPs)"""
attacks = [
log
for log in self.access_log
if log.get("attack_types") and not is_local_or_private_ip(log.get("ip", ""))
]
return attacks[-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()]
"""Get IPs that accessed honeypot paths (excludes local/private IPs)"""
return [
(ip, paths)
for ip, paths in self.honeypot_triggered.items()
if not is_local_or_private_ip(ip)
]
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))
"""Get statistics summary from database."""
if not self.db:
raise RuntimeError("Database not available for dashboard stats")
# Get aggregate counts from database
stats = self.db.get_dashboard_counts()
# Add detailed lists from database
stats["top_ips"] = self.db.get_top_ips(10)
stats["top_paths"] = self.db.get_top_paths(10)
stats["top_user_agents"] = self.db.get_top_user_agents(10)
stats["recent_suspicious"] = self.db.get_recent_suspicious(20)
stats["honeypot_triggered_ips"] = self.db.get_honeypot_triggered_ips()
stats["attack_types"] = self.db.get_recent_attacks(20)
stats["credential_attempts"] = self.db.get_credential_attempts(limit=50)
return stats
def cleanup_memory(self) -> None:
"""
Clean up in-memory structures to prevent unbounded growth.
Should be called periodically (e.g., every 5 minutes).
Trimming strategy:
- Keep most recent N entries in logs
- Remove oldest entries when limit exceeded
- Clean expired ban entries from ip_page_visits
"""
# Trim access_log to max size (keep most recent)
if len(self.access_log) > self.max_access_log_size:
self.access_log = self.access_log[-self.max_access_log_size :]
# Trim credential_attempts to max size (keep most recent)
if len(self.credential_attempts) > self.max_credential_log_size:
self.credential_attempts = self.credential_attempts[
-self.max_credential_log_size :
]
# Clean expired ban entries from ip_page_visits
current_time = datetime.now()
ips_to_clean = []
for ip, data in self.ip_page_visits.items():
ban_timestamp = data.get("ban_timestamp")
if ban_timestamp is not None:
try:
ban_time = datetime.fromisoformat(ban_timestamp)
time_diff = (current_time - ban_time).total_seconds()
if time_diff > self.ban_duration_seconds:
# Ban expired, reset the entry
data["count"] = 0
data["ban_timestamp"] = None
except (ValueError, TypeError):
pass
# Optional: Remove IPs with zero activity (advanced cleanup)
# Comment out to keep indefinite history of zero-activity IPs
# ips_to_remove = [
# ip
# for ip, data in self.ip_page_visits.items()
# if data.get("count", 0) == 0 and data.get("ban_timestamp") is None
# ]
# for ip in ips_to_remove:
# del self.ip_page_visits[ip]
def get_memory_stats(self) -> Dict[str, int]:
"""
Get current memory usage statistics for monitoring.
Returns:
Dictionary with counts of in-memory items
"""
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(),
'attack_types': self.get_attack_type_accesses(20),
'credential_attempts': self.credential_attempts[-50:] # Last 50 attempts
"access_log_size": len(self.access_log),
"credential_attempts_size": len(self.credential_attempts),
"unique_ips_tracked": len(self.ip_counts),
"unique_paths_tracked": len(self.path_counts),
"unique_user_agents": len(self.user_agent_counts),
"unique_ip_page_visits": len(self.ip_page_visits),
"honeypot_triggered_ips": len(self.honeypot_triggered),
}

View File

@@ -19,13 +19,15 @@ class Wordlists:
def _load_config(self):
"""Load wordlists from JSON file"""
config_path = Path(__file__).parent.parent / 'wordlists.json'
config_path = Path(__file__).parent.parent / "wordlists.json"
try:
with open(config_path, 'r') as f:
with open(config_path, "r") as f:
return json.load(f)
except FileNotFoundError:
get_app_logger().warning(f"Wordlists file {config_path} not found, using default values")
get_app_logger().warning(
f"Wordlists file {config_path} not found, using default values"
)
return self._get_defaults()
except json.JSONDecodeError as e:
get_app_logger().warning(f"Invalid JSON in {config_path}: {e}")
@@ -36,28 +38,21 @@ class Wordlists:
return {
"usernames": {
"prefixes": ["admin", "user", "root"],
"suffixes": ["", "_prod", "_dev"]
"suffixes": ["", "_prod", "_dev"],
},
"passwords": {
"prefixes": ["P@ssw0rd", "Admin"],
"simple": ["test", "demo", "password"]
},
"emails": {
"domains": ["example.com", "test.com"]
},
"api_keys": {
"prefixes": ["sk_live_", "api_", ""]
"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"]
"hosts": ["localhost", "db.internal"],
},
"applications": {
"names": ["WebApp", "Dashboard"]
},
"users": {
"roles": ["Administrator", "User"]
}
"applications": {"names": ["WebApp", "Dashboard"]},
"users": {"roles": ["Administrator", "User"]},
"server_headers": ["Apache/2.4.41 (Ubuntu)", "nginx/1.18.0"],
}
@property
@@ -124,13 +119,22 @@ class Wordlists:
def server_errors(self):
return self._data.get("server_errors", {})
@property
def server_headers(self):
return self._data.get("server_headers", [])
@property
def attack_urls(self):
"""Deprecated: use attack_patterns instead. Returns attack_patterns for backward compatibility."""
return self._data.get("attack_patterns", {})
_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

View File

@@ -10,10 +10,10 @@ def detect_xss_pattern(input_string: str) -> bool:
return False
wl = get_wordlists()
xss_pattern = wl.attack_patterns.get('xss_attempt', '')
xss_pattern = wl.attack_patterns.get("xss_attempt", "")
if not xss_pattern:
xss_pattern = r'(<script|</script|javascript:|onerror=|onload=|onclick=|<iframe|<img|<svg|eval\(|alert\()'
xss_pattern = r"(<script|</script|javascript:|onerror=|onload=|onclick=|<iframe|<img|<svg|eval\(|alert\()"
return bool(re.search(xss_pattern, input_string, re.IGNORECASE))

150
tests/test_credentials.sh Executable file
View File

@@ -0,0 +1,150 @@
#!/bin/bash
# This script sends various POST requests with credentials to the honeypot
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
# Configuration
HOST="localhost"
PORT="5000"
BASE_URL="http://${HOST}:${PORT}"
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Krawl Credential Logging Test Script${NC}"
echo -e "${BLUE}========================================${NC}\n"
# Check if server is running
echo -e "${YELLOW}Checking if server is running on ${BASE_URL}...${NC}"
if ! curl -s -f "${BASE_URL}/health" > /dev/null 2>&1; then
echo -e "${RED}❌ Server is not running. Please start the Krawl server first.${NC}"
echo -e "${YELLOW}Run: python3 src/server.py${NC}"
exit 1
fi
echo -e "${GREEN}✓ Server is running${NC}\n"
# Test 1: Simple login form POST
echo -e "${YELLOW}Test 1: POST to /login with form data${NC}"
curl -s -X POST "${BASE_URL}/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin&password=admin123" \
> /dev/null
echo -e "${GREEN}✓ Sent: admin / admin123${NC}\n"
sleep 1
# Test 2: Admin panel login
echo -e "${YELLOW}Test 2: POST to /admin with credentials${NC}"
curl -s -X POST "${BASE_URL}/admin" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "user=root&pass=toor&submit=Login" \
> /dev/null
echo -e "${GREEN}✓ Sent: root / toor${NC}\n"
sleep 1
# Test 3: WordPress login attempt
echo -e "${YELLOW}Test 3: POST to /wp-login.php${NC}"
curl -s -X POST "${BASE_URL}/wp-login.php" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "log=wpuser&pwd=Password1&wp-submit=Log+In" \
> /dev/null
echo -e "${GREEN}✓ Sent: wpuser / Password1${NC}\n"
sleep 1
# Test 4: JSON formatted credentials
echo -e "${YELLOW}Test 4: POST to /api/login with JSON${NC}"
curl -s -X POST "${BASE_URL}/api/login" \
-H "Content-Type: application/json" \
-d '{"username":"apiuser","password":"apipass123","remember":true}' \
> /dev/null
echo -e "${GREEN}✓ Sent: apiuser / apipass123${NC}\n"
sleep 1
# Test 5: SSH-style login
echo -e "${YELLOW}Test 5: POST to /ssh with credentials${NC}"
curl -s -X POST "${BASE_URL}/ssh" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=sshuser&password=P@ssw0rd!" \
> /dev/null
echo -e "${GREEN}✓ Sent: sshuser / P@ssw0rd!${NC}\n"
sleep 1
# Test 6: Database admin
echo -e "${YELLOW}Test 6: POST to /phpmyadmin with credentials${NC}"
curl -s -X POST "${BASE_URL}/phpmyadmin" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "pma_username=dbadmin&pma_password=dbpass123&server=1" \
> /dev/null
echo -e "${GREEN}✓ Sent: dbadmin / dbpass123${NC}\n"
sleep 1
# Test 7: Multiple fields with email
echo -e "${YELLOW}Test 7: POST to /register with email${NC}"
curl -s -X POST "${BASE_URL}/register" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "email=test@example.com&username=newuser&password=NewPass123&confirm_password=NewPass123" \
> /dev/null
echo -e "${GREEN}✓ Sent: newuser / NewPass123 (email: test@example.com)${NC}\n"
sleep 1
# Test 8: FTP credentials
echo -e "${YELLOW}Test 8: POST to /ftp/login${NC}"
curl -s -X POST "${BASE_URL}/ftp/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "ftpuser=ftpadmin&ftppass=ftp123456" \
> /dev/null
echo -e "${GREEN}✓ Sent: ftpadmin / ftp123456${NC}\n"
sleep 1
# Test 9: Common brute force attempt
echo -e "${YELLOW}Test 9: Multiple attempts (simulating brute force)${NC}"
for i in {1..3}; do
curl -s -X POST "${BASE_URL}/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin&password=pass${i}" \
> /dev/null
echo -e "${GREEN}✓ Attempt $i: admin / pass${i}${NC}"
sleep 0.5
done
echo ""
sleep 1
# Test 10: Special characters in credentials
echo -e "${YELLOW}Test 10: POST with special characters${NC}"
curl -s -X POST "${BASE_URL}/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "username=user@domain.com" \
--data-urlencode "password=P@\$\$w0rd!#%" \
> /dev/null
echo -e "${GREEN}✓ Sent: user@domain.com / P@\$\$w0rd!#%${NC}\n"
echo -e "${BLUE}========================================${NC}"
echo -e "${GREEN}✓ All credential tests completed!${NC}"
echo -e "${BLUE}========================================${NC}\n"
echo -e "${YELLOW}Check the results:${NC}"
echo -e " 1. View the log file: ${GREEN}tail -20 logs/credentials.log${NC}"
echo -e " 2. View the dashboard: ${GREEN}${BASE_URL}/dashboard${NC}"
echo -e " 3. Check recent logs: ${GREEN}tail -20 logs/access.log ${NC}\n"
# Display last 10 credential entries if log file exists
if [ -f "src/logs/credentials.log" ]; then
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Last 10 Captured Credentials:${NC}"
echo -e "${BLUE}========================================${NC}"
tail -10 src/logs/credentials.log
echo ""
fi
echo -e "${YELLOW}💡 Tip: Open ${BASE_URL}/dashboard in your browser to see the credentials in real-time!${NC}"

View File

@@ -0,0 +1,572 @@
#!/usr/bin/env python3
"""
Test script to insert fake external IPs into the database for testing the dashboard.
This generates realistic-looking test data including:
- Access logs with various suspicious activities
- Credential attempts
- Attack detections (SQL injection, XSS, etc.)
- Category behavior changes for timeline demonstration
- Geolocation data fetched from API with reverse geocoded city names
- Real good crawler IPs (Googlebot, Bingbot, etc.)
Usage:
python test_insert_fake_ips.py [num_ips] [logs_per_ip] [credentials_per_ip] [--no-cleanup]
Examples:
python test_insert_fake_ips.py # Generate 20 IPs with defaults, cleanup DB first
python test_insert_fake_ips.py 30 # Generate 30 IPs with defaults
python test_insert_fake_ips.py 30 20 5 # Generate 30 IPs, 20 logs each, 5 credentials each
python test_insert_fake_ips.py --no-cleanup # Generate data without cleaning DB first
Note: This script will make API calls to fetch geolocation data, so it may take a while.
"""
import random
import time
import sys
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from pathlib import Path
import requests
# Add parent src directory to path so we can import database and logger
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from database import get_database
from logger import get_app_logger
from geo_utils import extract_city_from_coordinates
# ----------------------
# TEST DATA GENERATORS
# ----------------------
# Fake IPs for testing - geolocation data will be fetched from API
# These are real public IPs from various locations around the world
FAKE_IPS = [
# United States
"45.142.120.10",
"107.189.10.143",
"162.243.175.23",
"198.51.100.89",
# Europe
"185.220.101.45",
"195.154.133.20",
"178.128.83.165",
"87.251.67.90",
"91.203.5.165",
"46.105.57.169",
"217.182.143.207",
"188.166.123.45",
# Asia
"103.253.145.36",
"42.112.28.216",
"118.163.74.160",
"43.229.53.35",
"115.78.208.140",
"14.139.56.18",
"61.19.25.207",
"121.126.219.198",
"202.134.4.212",
"171.244.140.134",
# South America
"177.87.169.20",
"200.21.19.58",
"181.13.140.98",
"190.150.24.34",
# Middle East & Africa
"41.223.53.141",
"196.207.35.152",
"5.188.62.214",
"37.48.93.125",
"102.66.137.29",
# Australia & Oceania
"103.28.248.110",
"202.168.45.33",
# Additional European IPs
"94.102.49.190",
"213.32.93.140",
"79.137.79.167",
"37.9.169.146",
"188.92.80.123",
"80.240.25.198",
]
# Real good crawler IPs (Googlebot, Bingbot, etc.) - geolocation will be fetched from API
GOOD_CRAWLER_IPS = [
"66.249.66.1", # Googlebot
"66.249.79.23", # Googlebot
"40.77.167.52", # Bingbot
"157.55.39.145", # Bingbot
"17.58.98.100", # Applebot
"199.59.150.39", # Twitterbot
"54.236.1.15", # Amazon Bot
]
FAKE_PATHS = [
"/admin",
"/login",
"/admin/login",
"/api/users",
"/wp-admin",
"/.env",
"/config.php",
"/admin.php",
"/shell.php",
"/../../../etc/passwd",
"/sqlmap",
"/w00t.php",
"/shell",
"/joomla/administrator",
]
FAKE_USER_AGENTS = [
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
"Nmap Scripting Engine",
"curl/7.68.0",
"python-requests/2.28.1",
"sqlmap/1.6.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"ZmEu",
"nikto/2.1.6",
]
FAKE_CREDENTIALS = [
("admin", "admin"),
("admin", "password"),
("root", "123456"),
("test", "test"),
("guest", "guest"),
("user", "12345"),
]
ATTACK_TYPES = [
"sql_injection",
"xss_attempt",
"path_traversal",
"suspicious_pattern",
"credential_submission",
]
CATEGORIES = [
"attacker",
"bad_crawler",
"good_crawler",
"regular_user",
"unknown",
]
def generate_category_scores():
"""Generate random category scores."""
scores = {
"attacker": random.randint(0, 100),
"good_crawler": random.randint(0, 100),
"bad_crawler": random.randint(0, 100),
"regular_user": random.randint(0, 100),
"unknown": random.randint(0, 100),
}
return scores
def generate_analyzed_metrics():
"""Generate random analyzed metrics."""
return {
"request_frequency": random.uniform(0.1, 100.0),
"suspicious_patterns": random.randint(0, 20),
"credential_attempts": random.randint(0, 10),
"attack_diversity": random.uniform(0, 1.0),
}
def cleanup_database(db_manager, app_logger):
"""
Clean up all existing test data from the database.
Args:
db_manager: Database manager instance
app_logger: Logger instance
"""
from models import (
AccessLog,
CredentialAttempt,
AttackDetection,
IpStats,
CategoryHistory,
)
app_logger.info("=" * 60)
app_logger.info("Cleaning up existing database data")
app_logger.info("=" * 60)
session = db_manager.session
try:
# Delete all records from each table
deleted_attack_detections = session.query(AttackDetection).delete()
deleted_access_logs = session.query(AccessLog).delete()
deleted_credentials = session.query(CredentialAttempt).delete()
deleted_category_history = session.query(CategoryHistory).delete()
deleted_ip_stats = session.query(IpStats).delete()
session.commit()
app_logger.info(f"Deleted {deleted_access_logs} access logs")
app_logger.info(f"Deleted {deleted_attack_detections} attack detections")
app_logger.info(f"Deleted {deleted_credentials} credential attempts")
app_logger.info(f"Deleted {deleted_category_history} category history records")
app_logger.info(f"Deleted {deleted_ip_stats} IP statistics")
app_logger.info("✓ Database cleanup complete")
except Exception as e:
session.rollback()
app_logger.error(f"Error during database cleanup: {e}")
raise
finally:
db_manager.close_session()
def fetch_geolocation_from_api(ip: str, app_logger) -> tuple:
"""
Fetch geolocation data from the IP reputation API.
Uses the most recent result and extracts city from coordinates.
Args:
ip: IP address to lookup
app_logger: Logger instance
Returns:
Tuple of (country_code, city, asn, asn_org) or None if failed
"""
try:
api_url = "https://iprep.lcrawl.com/api/iprep/"
params = {"cidr": ip}
headers = {"Content-Type": "application/json"}
response = requests.get(api_url, headers=headers, params=params, timeout=10)
if response.status_code == 200:
payload = response.json()
if payload.get("results"):
results = payload["results"]
# Get the most recent result (first in list, sorted by record_added)
most_recent = results[0]
geoip_data = most_recent.get("geoip_data", {})
country_code = geoip_data.get("country_iso_code")
asn = geoip_data.get("asn_autonomous_system_number")
asn_org = geoip_data.get("asn_autonomous_system_organization")
# Extract city from coordinates using reverse geocoding
city = extract_city_from_coordinates(geoip_data)
return (country_code, city, asn, asn_org)
except requests.RequestException as e:
app_logger.warning(f"Failed to fetch geolocation for {ip}: {e}")
except Exception as e:
app_logger.error(f"Error processing geolocation for {ip}: {e}")
return None
def generate_fake_data(
num_ips: int = 20,
logs_per_ip: int = 15,
credentials_per_ip: int = 3,
include_good_crawlers: bool = True,
cleanup: bool = True,
):
"""
Generate and insert fake test data into the database.
Args:
num_ips: Number of unique fake IPs to generate (default: 20)
logs_per_ip: Number of access logs per IP (default: 15)
credentials_per_ip: Number of credential attempts per IP (default: 3)
include_good_crawlers: Whether to add real good crawler IPs with API-fetched geolocation (default: True)
cleanup: Whether to clean up existing database data before generating new data (default: True)
"""
db_manager = get_database()
app_logger = get_app_logger()
# Ensure database is initialized
if not db_manager._initialized:
db_manager.initialize()
# Clean up existing data if requested
if cleanup:
cleanup_database(db_manager, app_logger)
print() # Add blank line for readability
app_logger.info("=" * 60)
app_logger.info("Starting fake IP data generation for testing")
app_logger.info("=" * 60)
total_logs = 0
total_credentials = 0
total_attacks = 0
total_category_changes = 0
# Select random IPs from the pool
selected_ips = random.sample(FAKE_IPS, min(num_ips, len(FAKE_IPS)))
# Create a varied distribution of request counts for better visualization
# Some IPs with very few requests, some with moderate, some with high
request_counts = []
for i in range(len(selected_ips)):
if i < len(selected_ips) // 5: # 20% high-traffic IPs
count = random.randint(1000, 10000)
elif i < len(selected_ips) // 2: # 30% medium-traffic IPs
count = random.randint(100, 1000)
else: # 50% low-traffic IPs
count = random.randint(5, 100)
request_counts.append(count)
random.shuffle(request_counts) # Randomize the order
for idx, ip in enumerate(selected_ips):
current_logs_count = request_counts[idx]
app_logger.info(
f"\nGenerating data for IP: {ip} ({current_logs_count} requests)"
)
# Generate access logs for this IP
for _ in range(current_logs_count):
path = random.choice(FAKE_PATHS)
user_agent = random.choice(FAKE_USER_AGENTS)
is_suspicious = random.choice(
[True, False, False]
) # 33% chance of suspicious
is_honeypot = random.choice(
[True, False, False, False]
) # 25% chance of honeypot trigger
# Randomly decide if this log has attack detections
attack_types = None
if random.choice([True, False, False]): # 33% chance
num_attacks = random.randint(1, 3)
attack_types = random.sample(ATTACK_TYPES, num_attacks)
log_id = db_manager.persist_access(
ip=ip,
path=path,
user_agent=user_agent,
method=random.choice(["GET", "POST"]),
is_suspicious=is_suspicious,
is_honeypot_trigger=is_honeypot,
attack_types=attack_types,
)
if log_id:
total_logs += 1
if attack_types:
total_attacks += len(attack_types)
# Generate credential attempts for this IP
for _ in range(credentials_per_ip):
username, password = random.choice(FAKE_CREDENTIALS)
path = random.choice(["/login", "/admin/login", "/api/auth"])
cred_id = db_manager.persist_credential(
ip=ip,
path=path,
username=username,
password=password,
)
if cred_id:
total_credentials += 1
app_logger.info(f" ✓ Generated {current_logs_count} access logs")
app_logger.info(f" ✓ Generated {credentials_per_ip} credential attempts")
# Fetch geolocation data from API
app_logger.info(f" 🌍 Fetching geolocation from API...")
geo_data = fetch_geolocation_from_api(ip, app_logger)
if geo_data:
country_code, city, asn, asn_org = geo_data
db_manager.update_ip_rep_infos(
ip=ip,
country_code=country_code,
asn=asn if asn else 12345,
asn_org=asn_org or "Unknown",
list_on={},
city=city,
)
location_display = (
f"{city}, {country_code}" if city else country_code or "Unknown"
)
app_logger.info(
f" 📍 API-fetched geolocation: {location_display} ({asn_org or 'Unknown'})"
)
else:
app_logger.warning(f" ⚠ Could not fetch geolocation for {ip}")
# Small delay to be nice to the API
time.sleep(0.5)
# Trigger behavior/category changes to demonstrate timeline feature
# First analysis
initial_category = random.choice(CATEGORIES)
app_logger.info(
f" ⟳ Analyzing behavior - Initial category: {initial_category}"
)
db_manager.update_ip_stats_analysis(
ip=ip,
analyzed_metrics=generate_analyzed_metrics(),
category=initial_category,
category_scores=generate_category_scores(),
last_analysis=datetime.now(tz=ZoneInfo("UTC")),
)
total_category_changes += 1
# Small delay to ensure timestamps are different
time.sleep(0.1)
# Second analysis with potential category change (70% chance)
if random.random() < 0.7:
new_category = random.choice(
[c for c in CATEGORIES if c != initial_category]
)
app_logger.info(
f" ⟳ Behavior change detected: {initial_category}{new_category}"
)
db_manager.update_ip_stats_analysis(
ip=ip,
analyzed_metrics=generate_analyzed_metrics(),
category=new_category,
category_scores=generate_category_scores(),
last_analysis=datetime.now(tz=ZoneInfo("UTC")),
)
total_category_changes += 1
# Optional third change (40% chance)
if random.random() < 0.4:
final_category = random.choice(
[c for c in CATEGORIES if c != new_category]
)
app_logger.info(
f" ⟳ Another behavior change: {new_category}{final_category}"
)
time.sleep(0.1)
db_manager.update_ip_stats_analysis(
ip=ip,
analyzed_metrics=generate_analyzed_metrics(),
category=final_category,
category_scores=generate_category_scores(),
last_analysis=datetime.now(tz=ZoneInfo("UTC")),
)
total_category_changes += 1
# Add good crawler IPs with real geolocation from API
total_good_crawlers = 0
if include_good_crawlers:
app_logger.info("\n" + "=" * 60)
app_logger.info("Adding Good Crawler IPs with API-fetched geolocation")
app_logger.info("=" * 60)
for crawler_ip in GOOD_CRAWLER_IPS:
app_logger.info(f"\nProcessing Good Crawler: {crawler_ip}")
# Fetch real geolocation from API
geo_data = fetch_geolocation_from_api(crawler_ip, app_logger)
# Don't generate access logs for good crawlers to prevent re-categorization
# We'll just create the IP stats entry with the category set
app_logger.info(
f" ✓ Adding as good crawler (no logs to prevent re-categorization)"
)
# First, we need to create the IP in the database via persist_access
# (but we'll only create one minimal log entry)
db_manager.persist_access(
ip=crawler_ip,
path="/robots.txt", # Minimal, normal crawler behavior
user_agent="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
method="GET",
is_suspicious=False,
is_honeypot_trigger=False,
attack_types=None,
)
# Add geolocation if API fetch was successful
if geo_data:
country_code, city, asn, asn_org = geo_data
db_manager.update_ip_rep_infos(
ip=crawler_ip,
country_code=country_code,
asn=asn if asn else 12345,
asn_org=asn_org,
list_on={},
city=city,
)
app_logger.info(
f" 📍 API-fetched geolocation: {city}, {country_code} ({asn_org})"
)
else:
app_logger.warning(f" ⚠ Could not fetch geolocation for {crawler_ip}")
# Set category to good_crawler - this sets manual_category=True to prevent re-analysis
db_manager.update_ip_stats_analysis(
ip=crawler_ip,
analyzed_metrics={
"request_frequency": 0.1, # Very low frequency
"suspicious_patterns": 0,
"credential_attempts": 0,
"attack_diversity": 0.0,
},
category="good_crawler",
category_scores={
"attacker": 0,
"good_crawler": 100,
"bad_crawler": 0,
"regular_user": 0,
"unknown": 0,
},
last_analysis=datetime.now(tz=ZoneInfo("UTC")),
)
total_good_crawlers += 1
time.sleep(0.5) # Small delay between API calls
# Print summary
app_logger.info("\n" + "=" * 60)
app_logger.info("Test Data Generation Complete!")
app_logger.info("=" * 60)
app_logger.info(f"Total IPs created: {len(selected_ips) + total_good_crawlers}")
app_logger.info(f" - Attackers/Mixed: {len(selected_ips)}")
app_logger.info(f" - Good Crawlers: {total_good_crawlers}")
app_logger.info(f"Total access logs: {total_logs}")
app_logger.info(f"Total attack detections: {total_attacks}")
app_logger.info(f"Total credential attempts: {total_credentials}")
app_logger.info(f"Total category changes: {total_category_changes}")
app_logger.info("=" * 60)
app_logger.info("\nYou can now view the dashboard with this test data.")
app_logger.info(
"The 'Behavior Timeline' will show category transitions for each IP."
)
app_logger.info(
"All IPs have API-fetched geolocation with reverse geocoded city names."
)
app_logger.info("Run: python server.py")
app_logger.info("=" * 60)
if __name__ == "__main__":
import sys
# Allow command-line arguments for customization
num_ips = int(sys.argv[1]) if len(sys.argv) > 1 else 20
logs_per_ip = int(sys.argv[2]) if len(sys.argv) > 2 else 15
credentials_per_ip = int(sys.argv[3]) if len(sys.argv) > 3 else 3
# Add --no-cleanup flag to skip database cleanup
cleanup = "--no-cleanup" not in sys.argv
generate_fake_data(
num_ips,
logs_per_ip,
credentials_per_ip,
include_good_crawlers=True,
cleanup=cleanup,
)

View File

@@ -353,10 +353,21 @@
}
},
"attack_patterns": {
"path_traversal": "\\.\\.",
"path_traversal": "(\\.\\.|%2e%2e|%252e%252e|\\.{2,}|%c0%ae|%c1%9c)",
"sql_injection": "('|\"|`|--|#|/\\*|\\*/|\\bunion\\b|\\bunion\\s+select\\b|\\bor\\b.*=.*|\\band\\b.*=.*|'.*or.*'.*=.*'|\\bsleep\\b|\\bwaitfor\\b|\\bdelay\\b|\\bbenchmark\\b|;.*select|;.*drop|;.*insert|;.*update|;.*delete|\\bexec\\b|\\bexecute\\b|\\bxp_cmdshell\\b|information_schema|table_schema|table_name)",
"xss_attempt": "(<script|</script|javascript:|onerror=|onload=|onclick=|onmouseover=|onfocus=|onblur=|<iframe|<img|<svg|<embed|<object|<body|<input|eval\\(|alert\\(|prompt\\(|confirm\\(|document\\.|window\\.|<style|expression\\(|vbscript:|data:text/html)",
"common_probes": "(wp-admin|phpmyadmin|\\.env|\\.git|/admin|/config)",
"shell_injection": "(\\||;|`|\\$\\(|&&)"
}
"shell_injection": "(\\||;|`|\\$\\(|&&|\\bnc\\b|\\bnetcat\\b|\\bwget\\b|\\bcurl\\b|/bin/bash|/bin/sh|cmd\\.exe)",
"lfi_rfi": "(file://|php://|expect://|data://|zip://|phar://|/etc/passwd|/etc/shadow|/proc/self|c:\\\\windows)",
"xxe_injection": "(<!ENTITY|<!DOCTYPE|SYSTEM|PUBLIC)",
"ldap_injection": "(\\*\\)|\\(\\||\\(&)",
"command_injection": "(&&|\\|\\||;|\\$\\{|\\$\\(|`)"
},
"server_headers": [
"Apache/2.4.41 (Ubuntu)",
"nginx/1.18.0",
"Microsoft-IIS/10.0",
"cloudflare",
"AmazonS3",
"gunicorn/20.1.0"
]
}