diff --git a/installer.sh b/installer.sh index 3f090b4..81b2619 100755 --- a/installer.sh +++ b/installer.sh @@ -114,6 +114,10 @@ create_user_and_dirs() { useradd --system --home "$APP_ROOT" --shell /usr/sbin/nologin "$APP_USER" fi + if id www-data >/dev/null 2>&1; then + usermod -aG "$APP_GROUP" www-data + fi + mkdir -p \ "$APP_ROOT/app/admin" \ "$APP_ROOT/app/decoy" \ @@ -129,9 +133,17 @@ create_user_and_dirs() { "$OPENCANARY_CONF_DIR" touch "$APP_LOG" + chown -R "$APP_USER:$APP_GROUP" "$APP_ROOT" - chmod 750 "$APP_ROOT" - chmod 770 "$APP_ROOT/db" "$APP_ROOT/logs" "$APP_ROOT/config" + + chmod 755 "$APP_ROOT" + chmod 755 "$APP_ROOT/app" + chmod 755 "$APP_ROOT/app/admin" + chmod 755 "$APP_ROOT/app/decoy" + chmod 755 "$APP_ROOT/app/common" + chmod 755 "$APP_ROOT/app/reports" + + chmod 775 "$APP_ROOT/db" "$APP_ROOT/logs" "$APP_ROOT/config" } install_python_env() { @@ -224,8 +236,9 @@ INSERT OR IGNORE INTO settings(key, value) VALUES('admin_expires_at', ''); INSERT OR IGNORE INTO settings(key, value) VALUES('appliance_name', 'BaldCanary Appliance'); SQL - chown "$APP_USER:$APP_GROUP" "$APP_DB" "$APP_ROOT/db" -R - chmod 660 "$APP_DB" +chown -R "$APP_USER:$APP_GROUP" "$APP_ROOT/db" +chmod 775 "$APP_ROOT/db" +chmod 660 "$APP_DB" } write_common_php() { @@ -329,6 +342,48 @@ function bc_detection_for_request(): array { return ['page_view', null]; } + +function bc_event_label(?string $type): string { + $type = trim((string)$type); + + $labels = [ + // BaldCanary web events + 'page_view' => 'Page View', + 'form_submit' => 'Form Submission', + 'xss_probe' => 'XSS Probe', + 'sql_injection_probe' => 'SQL Injection Probe', + 'command_injection_probe' => 'Command Injection Probe', + 'path_traversal_probe' => 'Path Traversal Probe', + 'sensitive_file_probe' => 'Sensitive File Probe', + 'session_file_probe' => 'Session File Probe', + 'exposed_session_directory' => 'Exposed Session Directory', + 'php_session_file' => 'PHP Session File Access', + 'backup_directory' => 'Backup Directory Access', + 'mysql_backup_directory' => 'MySQL Backup Directory Access', + 'api_docs' => 'API Documentation Access', + 'swagger_docs' => 'Swagger Documentation Access', + 'phpinfo_probe' => 'PHP Info Probe', + 'env_file_probe' => 'Environment File Probe', + 'config_file_probe' => 'Config File Probe', + + // OpenCanary common numeric log types + '1001' => 'OpenCanary Started', + '1002' => 'OpenCanary Stopped', + '1003' => 'OpenCanary Error', + '18001' => 'RDP Connection', + ]; + + if (isset($labels[$type])) { + return $labels[$type]; + } + + // Friendly fallback: "some_event_name" -> "Some Event Name" + if (preg_match('/^[a-z0-9_\\-\\.]+$/i', $type)) { + return ucwords(str_replace(['_', '-', '.'], ' ', $type)); + } + + return $type !== '' ? $type : 'Unknown Event'; +} PHP chown -R "$APP_USER:$APP_GROUP" "$APP_ROOT/app/common" @@ -543,7 +598,7 @@ $webhooks = $db->query('SELECT * FROM webhook_targets ORDER BY id DESC')->fetchA - + @@ -581,7 +636,7 @@ body{font-family:Arial,sans-serif;margin:40px;color:#222}.cover{border-bottom:4p

BaldCanary recorded interaction with the selected deception profile and exposed canary services. Events below may indicate penetration test activity, unauthorized curiosity, automated scanning, or attempted exploitation.

Event Timeline

- +
TimeSeverityTypeSource IPMethodPathBait
PHP @@ -789,7 +844,7 @@ write_opencanary_config() { "ftp.enabled": true, "ftp.port": 21, "ftp.banner": "FTP server ready", - "ssh.enabled": true, + "ssh.enabled": false, "ssh.port": 22, "ssh.version": "SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6", "http.enabled": false, @@ -820,13 +875,6 @@ write_opencanary_config() { "file": { "class": "logging.FileHandler", "filename": "/opt/baldcanary/logs/opencanary.log" - }, - "Webhook": { - "class": "opencanary.logger.WebhookHandler", - "url": "http://127.0.0.1:65534/opencanary", - "method": "POST", - "data": {"message": "%(message)s"}, - "status_code": 200 } } } @@ -842,49 +890,132 @@ write_collector() { cat > "$APP_ROOT/scripts/opencanary_collector.py" <<'PY' #!/opt/baldcanary/venv/bin/python -from http.server import BaseHTTPRequestHandler, HTTPServer import json +import os import sqlite3 +import time DB = '/opt/baldcanary/db/baldcanary.sqlite' +LOG = '/opt/baldcanary/logs/opencanary.log' +STATE = '/opt/baldcanary/logs/opencanary.offset' -class Handler(BaseHTTPRequestHandler): - def do_POST(self): - length = int(self.headers.get('content-length', 0)) - body = self.rfile.read(length).decode('utf-8', errors='replace') - event = {'raw': body} - try: - outer = json.loads(body) - msg = outer.get('message', body) - if isinstance(msg, str): - try: - event = json.loads(msg) - except Exception: - event = {'message': msg} - elif isinstance(msg, dict): - event = msg - except Exception: - pass +def read_offset(): + try: + with open(STATE, 'r') as f: + return int(f.read().strip() or '0') + except Exception: + return 0 - con = sqlite3.connect(DB) - con.execute('''INSERT INTO events(source,severity,event_type,src_ip,path,raw_json) - VALUES(?,?,?,?,?,?)''', ( - 'opencanary', 'high', str(event.get('logtype', 'opencanary_event')), - str(event.get('src_host', event.get('src_ip', ''))), - str(event.get('dst_port', '')), - json.dumps(event) - )) - con.commit() - con.close() - self.send_response(200) - self.end_headers() - self.wfile.write(b'ok') +def write_offset(offset): + with open(STATE, 'w') as f: + f.write(str(offset)) - def log_message(self, fmt, *args): +def parse_line(line): + line = line.strip() + if not line: + return None + + try: + return json.loads(line) + except Exception: + return { + 'message': line, + 'logtype': 'opencanary_event' + } + +def event_fields(event): + event_type = str( + event.get('logtype') + or event.get('eventid') + or event.get('event_type') + or 'opencanary_event' + ) + + src_ip = str( + event.get('src_host') + or event.get('src_ip') + or event.get('src_host_reverse') + or '' + ) + + dst_port = str( + event.get('dst_port') + or event.get('local_port') + or '' + ) + + logdata = event.get('logdata') or {} + username = '' + password = '' + + if isinstance(logdata, dict): + username = str(logdata.get('USERNAME') or logdata.get('username') or '') + password = str(logdata.get('PASSWORD') or logdata.get('password') or '') + + matched_bait = '' + if username or password: + matched_bait = f"username={username} password={password}".strip() + + path = f"port:{dst_port}" if dst_port else '' + + return event_type, src_ip, path, matched_bait + +def insert_event(event): + event_type, src_ip, path, matched_bait = event_fields(event) + + # Ignore OpenCanary internal/status noise. + # logtype/event 1001 with dst_port -1 is not attacker activity. + dst_port = str(event.get('dst_port') or event.get('local_port') or '') + if str(event_type) == '1001' and dst_port in ('-1', '') and not src_ip: return + con = sqlite3.connect(DB) + con.execute( + '''INSERT INTO events(source,severity,event_type,src_ip,path,matched_bait,raw_json) + VALUES(?,?,?,?,?,?,?)''', + ( + 'opencanary', + 'high', + event_type, + src_ip, + path, + matched_bait, + json.dumps(event) + ) + ) + con.commit() + con.close() + +def main(): + os.makedirs(os.path.dirname(LOG), exist_ok=True) + open(LOG, 'a').close() + + offset = read_offset() + + while True: + try: + size = os.path.getsize(LOG) + if size < offset: + offset = 0 + + with open(LOG, 'r') as f: + f.seek(offset) + for line in f: + event = parse_line(line) + if event: + insert_event(event) + offset = f.tell() + write_offset(offset) + + except Exception as exc: + # Avoid crashing the service; systemd should not need to restart us for transient parse/db issues. + with open('/opt/baldcanary/logs/collector-error.log', 'a') as err: + err.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} {exc}\\n") + + time.sleep(2) + if __name__ == '__main__': - HTTPServer(('127.0.0.1', 65534), Handler).serve_forever() + main() PY chmod +x "$APP_ROOT/scripts/opencanary_collector.py" @@ -898,14 +1029,15 @@ write_systemd_units() { [Unit] Description=OpenCanary daemon for BaldCanary After=network.target baldcanary-decoylog.service +Wants=baldcanary-decoylog.service [Service] Type=simple User=root -ExecStart=${APP_ROOT}/venv/bin/opencanaryd --start --uid=root --gid=root -Restart=always -RestartSec=5 WorkingDirectory=${APP_ROOT} +ExecStart=${APP_ROOT}/venv/bin/opencanaryd --dev +Restart=on-failure +RestartSec=5 [Install] WantedBy=multi-user.target @@ -913,7 +1045,7 @@ EOF cat > /etc/systemd/system/baldcanary-decoylog.service <&2 + setup_complete="$(get_setting setup_complete || true)" + + if [[ -z "$profile" && "$setup_complete" == "1" ]]; then + echo "No deception profile is set. Set one before enabling Admin Mode." >&2 exit 1 fi + if [[ -z "$profile" ]]; then + profile="First-run setup pending" + fi if [[ -z "$PHP_FPM_SOCK" ]]; then echo "PHP-FPM socket not found." >&2 exit 1 @@ -1228,7 +1365,7 @@ write_sudoers() { ensure_sudo_available cat > /etc/sudoers.d/baldcanary <<'EOF' -www-data ALL=(root) NOPASSWD: /usr/local/bin/baldcanary admin off +www-data ALL=(root) NOPASSWD: /usr/local/bin/baldcanary admin off, /usr/local/bin/baldcanary admin off --systemd EOF chmod 440 /etc/sudoers.d/baldcanary