diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000000..41a281e7f49 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,51 @@ +# Git +.git +.gitignore +.github + +# Docker +docker-compose.yml +Dockerfile +.dockerignore + +# DB +mongodata +pgdata +*.db +*.sqlite3 + +# Python +__pycache__ +*.py[cod] +*$py.class +*.so +.Python +*.egg +*.egg-info +dist +build +.eggs +.venv +venv +env + +# IDE +.vscode +.idea +*.swp +*.swo +*~ + +# Logs +*.log +logs + +# OS +.DS_Store +Thumbs.db + +# Others +.env.local +.cache +tmp +temp \ No newline at end of file diff --git a/analyzer/windows/modules/auxiliary/screenshots.py b/analyzer/windows/modules/auxiliary/screenshots.py index e254e7a5cec..f93c93986cc 100644 --- a/analyzer/windows/modules/auxiliary/screenshots.py +++ b/analyzer/windows/modules/auxiliary/screenshots.py @@ -3,14 +3,29 @@ # See the file 'docs/LICENSE' for copying permission. import logging +import os import time +from contextlib import suppress from io import BytesIO from threading import Thread +try: + from PIL import Image +except ImportError: + pass + from lib.api.screenshot import Screenshot from lib.common.abstracts import Auxiliary from lib.common.results import NetlogFile +HAVE_CV2 = False +with suppress(ImportError): + import cv2 + import numpy as np + + HAVE_CV2 = True + + log = logging.getLogger(__name__) SHOT_DELAY = 1 @@ -20,6 +35,26 @@ SKIP_AREA = None +def handle_qr_codes(image_data): + """Extract URL from QR code if present.""" + if not HAVE_CV2: + return None + + try: + image = Image.open(image_data) + # Convert PIL image to BGR numpy array for OpenCV + img = cv2.cvtColor(np.array(image.convert("RGB")), cv2.COLOR_RGB2BGR) + detector = cv2.QRCodeDetector() + extracted, _, _ = detector.detectAndDecode(img) + # Simple URL detection + if extracted and "://" in extracted[:10]: + return extracted + except Exception as e: + log.debug("Error in handle_qr_codes: %s", e) + + return None + + class Screenshots(Auxiliary, Thread): """Take screenshots.""" @@ -27,6 +62,7 @@ def __init__(self, options, config): Auxiliary.__init__(self, options, config) Thread.__init__(self) self.enabled = config.screenshots_windows + self.screenshots_qr = getattr(config, "screenshots_qr", False) self.do_run = self.enabled def stop(self): @@ -62,7 +98,17 @@ def run(self): img_current.save(tmpio, format="JPEG") tmpio.seek(0) - # now upload to host from the StringIO + if self.screenshots_qr and HAVE_CV2: + url = handle_qr_codes(tmpio) + if url: + log.info("QR code detected with URL: %s", url) + try: + # os.startfile is Windows only and usually works for URLs + os.startfile(url) + except Exception as e: + log.error("Failed to open QR URL: %s", e) + tmpio.seek(0) + nf = NetlogFile() nf.init(f"shots/{str(img_counter).rjust(4, '0')}.jpg") for chunk in tmpio: diff --git a/conf/default/auxiliary.conf.default b/conf/default/auxiliary.conf.default index 94a5707242b..bc2c09d6362 100644 --- a/conf/default/auxiliary.conf.default +++ b/conf/default/auxiliary.conf.default @@ -32,6 +32,7 @@ procmon = no recentfiles = no screenshots_windows = yes screenshots_linux = yes +screenshots_qr = no sysmon_windows = no sysmon_linux = no tlsdump = yes diff --git a/dev_utils/mongo_hooks.py b/dev_utils/mongo_hooks.py index 6270a86763a..08427a7ec31 100644 --- a/dev_utils/mongo_hooks.py +++ b/dev_utils/mongo_hooks.py @@ -1,7 +1,10 @@ import itertools import logging +from contextlib import suppress from pymongo import UpdateOne, errors +from pymongo.errors import InvalidDocument, BulkWriteError +import bson from dev_utils.mongodb import ( mongo_bulk_write, @@ -61,13 +64,12 @@ def normalize_file(file_dict, task_id): ) new_dict = {} for fld in static_fields: - try: + with suppress(KeyError): new_dict[fld] = file_dict.pop(fld) - except KeyError: - pass new_dict["_id"] = key file_dict[FILE_REF_KEY] = key + return UpdateOne({"_id": key}, {"$set": new_dict, "$addToSet": {TASK_IDS_KEY: task_id}}, upsert=True, hint=[("_id", 1)]) @@ -87,8 +89,32 @@ def normalize_files(report): try: if requests: mongo_bulk_write(FILES_COLL, requests, ordered=False) - except errors.OperationFailure as exc: - log.error("Mongo hook 'normalize_files' failed with code %d: %s", exc.code, exc) + except (errors.OperationFailure, InvalidDocument, BulkWriteError) as exc: + log.warning("Mongo hook 'normalize_files' failed: %s. Attempting to sanitize strings and retry.", exc) + for req in requests: + # req._doc is the update document: {"$set": new_dict, ...} + # Accessing private attribute _doc to modify in place for retry + try: + if hasattr(req, "_doc") and "$set" in req._doc and "strings" in req._doc["$set"]: + strings_val = req._doc["$set"]["strings"] + # Check if strings field alone is too large (buffer safe 15MB) + if strings_val and len(bson.encode({"strings": strings_val})) > 15 * 1024 * 1024: + log.warning("Truncating oversized strings field for retry.") + if isinstance(strings_val, list): + req._doc["$set"]["strings"] = strings_val[:1000] + else: + req._doc["$set"]["strings"] = [] + # If still too large, clear it + if len(bson.encode({"strings": req._doc["$set"]["strings"]})) > 15 * 1024 * 1024: + req._doc["$set"]["strings"] = [] + except Exception as e: + log.error("Failed to sanitize request during retry: %s", e) + + # Retry the bulk write + try: + mongo_bulk_write(FILES_COLL, requests, ordered=False) + except Exception as retry_exc: + log.error("Retry of 'normalize_files' failed: %s", retry_exc) return report diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 00000000000..ac45eb4a119 --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,8 @@ +WEB_PORT=8000 +RESULT_PORT=2042 +PG_PORT=5432 +MONGO_PORT=27017 + +POSTGRES_USER=cape +POSTGRES_PASSWORD=cape +POSTGRES_DB=cape diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000000..5e8975ddc89 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,36 @@ +FROM python:3.11-bookworm + +RUN apt-get update \ + && apt-get install -y --no-install-recommends git libgraphviz-dev tcpdump libcap2-bin iproute2 libjansson-dev libmagic-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN useradd -ms /bin/bash cape + +RUN pip install --no-cache-dir poetry + +RUN poetry config virtualenvs.create false + +RUN mkdir -p /etc/poetry/bin && ln -s $(which poetry) /etc/poetry/bin/poetry +RUN mkdir -p /opt && ln -s /cape /opt/CAPEv2 + +WORKDIR /cape + +COPY pyproject.toml poetry.lock* ./ + +RUN poetry install --no-interaction --no-ansi --no-root + +COPY . . + +RUN poetry install --no-interaction --no-ansi + +RUN pip install --no-cache-dir -U flare-floss +RUN bash extra/yara_installer.sh + +RUN bash docker/pcap.sh + +RUN bash conf/copy_configs.sh +RUN chown -R cape:cape /cape + +USER cape + +CMD ["bash", "docker/run.sh"] \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 00000000000..f94336ced5f --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,67 @@ +services: + cape-db: + image: postgres:bookworm + hostname: cape-db + restart: unless-stopped + ports: + - "127.0.0.1:${PG_PORT:-5432}:5432" + environment: + POSTGRES_USER: ${POSTGRES_USER:-cape} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-cape} + POSTGRES_DB: ${POSTGRES_DB:-cape} + PGDATA: /var/lib/postgresql/data/pgdata + volumes: + - cape-db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-cape} -d ${POSTGRES_DB:-cape}"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 30s + + mongodb: + image: mongo:6 + command: ["--bind_ip_all"] + volumes: + - cape-mongo-data:/data/db + ports: + - "127.0.0.1:${MONGO_PORT:-27017}:27017" + restart: unless-stopped + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.runCommand({ ping: 1 })"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 20s + + cape-server: + build: + context: ../ + dockerfile: docker/Dockerfile + hostname: cape-server + restart: unless-stopped + depends_on: + cape-db: + condition: service_healthy + mongodb: + condition: service_healthy + environment: + - WEB_PORT=${WEB_PORT:-8000} + - POSTGRES_USER=${POSTGRES_USER:-cape} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-cape} + - POSTGRES_DB=${POSTGRES_DB:-cape} + ports: + - "127.0.0.1:${RESULT_PORT:-2042}:2042" # result server + - "127.0.0.1:${WEB_PORT:-8000}:8000" # web ui + volumes: + - ../conf:/cape/conf + - ../custom/conf:/cape/custom/conf + - ../custom:/cape/custom + - ../storage:/cape/storage + cap_add: + - NET_ADMIN + - NET_RAW + +volumes: + cape-db-data: + cape-mongo-data: diff --git a/docker/pcap.sh b/docker/pcap.sh new file mode 100644 index 00000000000..a83b34c0315 --- /dev/null +++ b/docker/pcap.sh @@ -0,0 +1,4 @@ +groupadd pcap +usermod -a -G pcap cape +chgrp pcap /usr/bin/tcpdump +setcap cap_net_raw,cap_net_admin=eip /usr/bin/tcpdump \ No newline at end of file diff --git a/docker/readme.md b/docker/readme.md new file mode 100644 index 00000000000..7edb30667cf --- /dev/null +++ b/docker/readme.md @@ -0,0 +1,4 @@ +This is not official docker soluction! +Is community based contribution so use on your own risks! + +No support here from core devs! diff --git a/docker/run.sh b/docker/run.sh new file mode 100644 index 00000000000..dcb91965326 --- /dev/null +++ b/docker/run.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -e + +cd /cape + +# Initialize configs if mounted volume is empty +if [ ! -f "conf/cuckoo.conf" ]; then + echo "Initializing configuration files..." + bash conf/copy_configs.sh +fi + +# Configure Database connection for Docker environment +mkdir -p conf/cuckoo.conf.d +DB_CONF="conf/cuckoo.conf.d/00_docker_db.conf" +if [ ! -f "$DB_CONF" ]; then + echo "Creating Docker DB configuration..." + cat > "$DB_CONF" <Advance id="duringScript" name="during_script"> {% endif %} - +
+ +
+
+