First working version

This commit is contained in:
2026-05-08 16:05:13 -04:00
parent 2af92f274d
commit ec4853b008

View File

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