First working version
This commit is contained in:
239
installer.sh
239
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
|
||||
<tr>
|
||||
<td><?=h($event['event_time'])?></td>
|
||||
<td><span class="badge text-bg-secondary"><?=h($event['severity'])?></span></td>
|
||||
<td><?=h($event['event_type'])?></td>
|
||||
<td><?=h(bc_event_label($event['event_type']))?></td>
|
||||
<td><?=h($event['src_ip'])?></td>
|
||||
<td class="text-break"><?=h($event['path'])?></td>
|
||||
</tr>
|
||||
@ -581,7 +636,7 @@ body{font-family:Arial,sans-serif;margin:40px;color:#222}.cover{border-bottom:4p
|
||||
<p>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.</p>
|
||||
<h2>Event Timeline</h2>
|
||||
<table><thead><tr><th>Time</th><th>Severity</th><th>Type</th><th>Source IP</th><th>Method</th><th>Path</th><th>Bait</th></tr></thead><tbody>
|
||||
<?php foreach ($events as $e): ?><tr><td><?=h($e['event_time'])?></td><td><?=h($e['severity'])?></td><td><?=h($e['event_type'])?></td><td><?=h($e['src_ip'])?></td><td><?=h($e['method'])?></td><td><?=h($e['path'])?></td><td><?=h($e['matched_bait'])?></td></tr><?php endforeach; ?>
|
||||
<?php foreach ($events as $e): ?><tr><td><?=h($e['event_time'])?></td><td><?=h($e['severity'])?></td><td><?=h(bc_event_label($e['event_type']))?></td><td><?=h($e['src_ip'])?></td><td><?=h($e['method'])?></td><td><?=h($e['path'])?></td><td><?=h($e['matched_bait'])?></td></tr><?php endforeach; ?>
|
||||
</tbody></table>
|
||||
</body></html>
|
||||
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}
|
||||
def read_offset():
|
||||
try:
|
||||
outer = json.loads(body)
|
||||
msg = outer.get('message', body)
|
||||
if isinstance(msg, str):
|
||||
with open(STATE, 'r') as f:
|
||||
return int(f.read().strip() or '0')
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def write_offset(offset):
|
||||
with open(STATE, 'w') as f:
|
||||
f.write(str(offset))
|
||||
|
||||
def parse_line(line):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
return None
|
||||
|
||||
try:
|
||||
event = json.loads(msg)
|
||||
return json.loads(line)
|
||||
except Exception:
|
||||
event = {'message': msg}
|
||||
elif isinstance(msg, dict):
|
||||
event = msg
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
'message': line,
|
||||
'logtype': 'opencanary_event'
|
||||
}
|
||||
|
||||
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 event_fields(event):
|
||||
event_type = str(
|
||||
event.get('logtype')
|
||||
or event.get('eventid')
|
||||
or event.get('event_type')
|
||||
or 'opencanary_event'
|
||||
)
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
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 <<EOF
|
||||
[Unit]
|
||||
Description=BaldCanary local event collector and alert dispatcher
|
||||
Description=BaldCanary OpenCanary log ingestor
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
@ -1059,10 +1191,15 @@ admin_on() {
|
||||
require_root
|
||||
local profile
|
||||
profile="$(get_setting active_profile || true)"
|
||||
if [[ -z "$profile" ]]; then
|
||||
echo "No deception profile is set yet. Complete first-run setup before using temporary Admin Mode." >&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
|
||||
|
||||
Reference in New Issue
Block a user