First working version
This commit is contained in:
245
installer.sh
245
installer.sh
@ -114,6 +114,10 @@ create_user_and_dirs() {
|
|||||||
useradd --system --home "$APP_ROOT" --shell /usr/sbin/nologin "$APP_USER"
|
useradd --system --home "$APP_ROOT" --shell /usr/sbin/nologin "$APP_USER"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if id www-data >/dev/null 2>&1; then
|
||||||
|
usermod -aG "$APP_GROUP" www-data
|
||||||
|
fi
|
||||||
|
|
||||||
mkdir -p \
|
mkdir -p \
|
||||||
"$APP_ROOT/app/admin" \
|
"$APP_ROOT/app/admin" \
|
||||||
"$APP_ROOT/app/decoy" \
|
"$APP_ROOT/app/decoy" \
|
||||||
@ -129,9 +133,17 @@ create_user_and_dirs() {
|
|||||||
"$OPENCANARY_CONF_DIR"
|
"$OPENCANARY_CONF_DIR"
|
||||||
|
|
||||||
touch "$APP_LOG"
|
touch "$APP_LOG"
|
||||||
|
|
||||||
chown -R "$APP_USER:$APP_GROUP" "$APP_ROOT"
|
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() {
|
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');
|
INSERT OR IGNORE INTO settings(key, value) VALUES('appliance_name', 'BaldCanary Appliance');
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
chown "$APP_USER:$APP_GROUP" "$APP_DB" "$APP_ROOT/db" -R
|
chown -R "$APP_USER:$APP_GROUP" "$APP_ROOT/db"
|
||||||
chmod 660 "$APP_DB"
|
chmod 775 "$APP_ROOT/db"
|
||||||
|
chmod 660 "$APP_DB"
|
||||||
}
|
}
|
||||||
|
|
||||||
write_common_php() {
|
write_common_php() {
|
||||||
@ -329,6 +342,48 @@ function bc_detection_for_request(): array {
|
|||||||
|
|
||||||
return ['page_view', null];
|
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
|
PHP
|
||||||
|
|
||||||
chown -R "$APP_USER:$APP_GROUP" "$APP_ROOT/app/common"
|
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>
|
<tr>
|
||||||
<td><?=h($event['event_time'])?></td>
|
<td><?=h($event['event_time'])?></td>
|
||||||
<td><span class="badge text-bg-secondary"><?=h($event['severity'])?></span></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><?=h($event['src_ip'])?></td>
|
||||||
<td class="text-break"><?=h($event['path'])?></td>
|
<td class="text-break"><?=h($event['path'])?></td>
|
||||||
</tr>
|
</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>
|
<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>
|
<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>
|
<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>
|
</tbody></table>
|
||||||
</body></html>
|
</body></html>
|
||||||
PHP
|
PHP
|
||||||
@ -789,7 +844,7 @@ write_opencanary_config() {
|
|||||||
"ftp.enabled": true,
|
"ftp.enabled": true,
|
||||||
"ftp.port": 21,
|
"ftp.port": 21,
|
||||||
"ftp.banner": "FTP server ready",
|
"ftp.banner": "FTP server ready",
|
||||||
"ssh.enabled": true,
|
"ssh.enabled": false,
|
||||||
"ssh.port": 22,
|
"ssh.port": 22,
|
||||||
"ssh.version": "SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6",
|
"ssh.version": "SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6",
|
||||||
"http.enabled": false,
|
"http.enabled": false,
|
||||||
@ -820,13 +875,6 @@ write_opencanary_config() {
|
|||||||
"file": {
|
"file": {
|
||||||
"class": "logging.FileHandler",
|
"class": "logging.FileHandler",
|
||||||
"filename": "/opt/baldcanary/logs/opencanary.log"
|
"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'
|
cat > "$APP_ROOT/scripts/opencanary_collector.py" <<'PY'
|
||||||
#!/opt/baldcanary/venv/bin/python
|
#!/opt/baldcanary/venv/bin/python
|
||||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
import time
|
||||||
|
|
||||||
DB = '/opt/baldcanary/db/baldcanary.sqlite'
|
DB = '/opt/baldcanary/db/baldcanary.sqlite'
|
||||||
|
LOG = '/opt/baldcanary/logs/opencanary.log'
|
||||||
|
STATE = '/opt/baldcanary/logs/opencanary.offset'
|
||||||
|
|
||||||
class Handler(BaseHTTPRequestHandler):
|
def read_offset():
|
||||||
def do_POST(self):
|
try:
|
||||||
length = int(self.headers.get('content-length', 0))
|
with open(STATE, 'r') as f:
|
||||||
body = self.rfile.read(length).decode('utf-8', errors='replace')
|
return int(f.read().strip() or '0')
|
||||||
event = {'raw': body}
|
except Exception:
|
||||||
try:
|
return 0
|
||||||
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
|
|
||||||
|
|
||||||
con = sqlite3.connect(DB)
|
def write_offset(offset):
|
||||||
con.execute('''INSERT INTO events(source,severity,event_type,src_ip,path,raw_json)
|
with open(STATE, 'w') as f:
|
||||||
VALUES(?,?,?,?,?,?)''', (
|
f.write(str(offset))
|
||||||
'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 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
|
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__':
|
if __name__ == '__main__':
|
||||||
HTTPServer(('127.0.0.1', 65534), Handler).serve_forever()
|
main()
|
||||||
PY
|
PY
|
||||||
|
|
||||||
chmod +x "$APP_ROOT/scripts/opencanary_collector.py"
|
chmod +x "$APP_ROOT/scripts/opencanary_collector.py"
|
||||||
@ -898,14 +1029,15 @@ write_systemd_units() {
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=OpenCanary daemon for BaldCanary
|
Description=OpenCanary daemon for BaldCanary
|
||||||
After=network.target baldcanary-decoylog.service
|
After=network.target baldcanary-decoylog.service
|
||||||
|
Wants=baldcanary-decoylog.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=root
|
User=root
|
||||||
ExecStart=${APP_ROOT}/venv/bin/opencanaryd --start --uid=root --gid=root
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
WorkingDirectory=${APP_ROOT}
|
WorkingDirectory=${APP_ROOT}
|
||||||
|
ExecStart=${APP_ROOT}/venv/bin/opencanaryd --dev
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
@ -913,7 +1045,7 @@ EOF
|
|||||||
|
|
||||||
cat > /etc/systemd/system/baldcanary-decoylog.service <<EOF
|
cat > /etc/systemd/system/baldcanary-decoylog.service <<EOF
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=BaldCanary local event collector and alert dispatcher
|
Description=BaldCanary OpenCanary log ingestor
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
@ -1059,10 +1191,15 @@ admin_on() {
|
|||||||
require_root
|
require_root
|
||||||
local profile
|
local profile
|
||||||
profile="$(get_setting active_profile || true)"
|
profile="$(get_setting active_profile || true)"
|
||||||
if [[ -z "$profile" ]]; then
|
setup_complete="$(get_setting setup_complete || true)"
|
||||||
echo "No deception profile is set yet. Complete first-run setup before using temporary Admin Mode." >&2
|
|
||||||
|
if [[ -z "$profile" && "$setup_complete" == "1" ]]; then
|
||||||
|
echo "No deception profile is set. Set one before enabling Admin Mode." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if [[ -z "$profile" ]]; then
|
||||||
|
profile="First-run setup pending"
|
||||||
|
fi
|
||||||
if [[ -z "$PHP_FPM_SOCK" ]]; then
|
if [[ -z "$PHP_FPM_SOCK" ]]; then
|
||||||
echo "PHP-FPM socket not found." >&2
|
echo "PHP-FPM socket not found." >&2
|
||||||
exit 1
|
exit 1
|
||||||
@ -1228,7 +1365,7 @@ write_sudoers() {
|
|||||||
ensure_sudo_available
|
ensure_sudo_available
|
||||||
|
|
||||||
cat > /etc/sudoers.d/baldcanary <<'EOF'
|
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
|
EOF
|
||||||
|
|
||||||
chmod 440 /etc/sudoers.d/baldcanary
|
chmod 440 /etc/sudoers.d/baldcanary
|
||||||
|
|||||||
Reference in New Issue
Block a user