87
.github/workflows/docker-build-push.yml
vendored
Normal 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
@@ -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
|
||||
57
.github/workflows/kubernetes-validation.yml
vendored
Normal 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
@@ -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
@@ -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
@@ -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/*
|
||||
16
Dockerfile
@@ -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
@@ -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 cloud‑native deception server designed to detect, delay, and analyze malicious web crawlers and automated scanners.
|
||||
**Krawl** is a cloud‑native deception server designed to detect, delay, and analyze malicious attackers, web crawlers and automated scanners.
|
||||
|
||||
It creates realistic fake web applications filled with low‑hanging 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
|
||||
|
||||

|
||||

|
||||
|
||||
## 🚀 Quick Start
|
||||
## Helm Chart
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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 honeypot’s 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>
|
||||

|
||||
|
||||
|
||||
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.
|
||||
|
||||

|
||||
The `/server` page displays randomly generated fake error information for each known server.
|
||||
|
||||

|
||||
|
||||
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>
|
||||

|
||||
|
||||
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>
|
||||

|
||||
|
||||
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**.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
@@ -292,14 +337,7 @@ The top IP Addresses is shown along with top paths and User Agents
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
```
|
||||

|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
|
||||
46
config.yaml
Normal 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
|
||||
@@ -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
@@ -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
@@ -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 "$@"
|
||||
@@ -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
@@ -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).
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 46 KiB |
BIN
img/credentials-and-passwords.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 76 KiB |
BIN
img/dashboard-3.png
Normal file
|
After Width: | Height: | Size: 206 KiB |
BIN
img/env-page.png
|
Before Width: | Height: | Size: 30 KiB |
BIN
img/geoip_dashboard.png
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
img/ip-reputation.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
95
img/krawl-svg.svg
Normal 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 |
|
Before Width: | Height: | Size: 119 KiB |
BIN
img/server-and-env-page.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
img/sql_injection.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
img/users-and-secrets.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
47
kubernetes/README.md
Normal 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>`
|
||||
@@ -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:
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -5,6 +5,7 @@ resources:
|
||||
- namespace.yaml
|
||||
- configmap.yaml
|
||||
- wordlists-configmap.yaml
|
||||
- pvc.yaml
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
- network-policy.yaml
|
||||
|
||||
@@ -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
|
||||
|
||||
16
kubernetes/manifests/pvc.yaml
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
257
src/config.py
@@ -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
@@ -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
@@ -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)
|
||||
866
src/handler.py
61
src/ip_utils.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
40
src/migrations/add_category_history.py
Normal 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
@@ -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
@@ -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()}
|
||||
125
src/server.py
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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
@@ -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}")
|
||||
71
src/tasks/memory_cleanup.py
Normal 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}")
|
||||
76
src/tasks/top_attacking_ips.py
Normal 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
@@ -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
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
106
src/templates/html/main_page.html
Normal 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>
|
||||
@@ -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)
|
||||
|
||||
95
src/templates/static/krawl-svg.svg
Normal 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 |
@@ -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]
|
||||
|
||||
|
||||
644
src/tracker.py
@@ -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,46 +162,134 @@ 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(),
|
||||
}
|
||||
)
|
||||
|
||||
# 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]:
|
||||
"""
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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}"
|
||||
572
tests/test_insert_fake_ips.py
Normal 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,
|
||||
)
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||