Files
Bjorn/comment.py

343 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# comment.py
# Comments manager with database backend
# Provides contextual messages for display with timing control and multilingual support.
# comment = ai.get_comment("SSHBruteforce", params={"user": "pi", "ip": "192.168.0.12"})
# Avec un texte DB du style: "Trying {user}@{ip} over SSH..."
import os
import time
import random
import locale
from typing import Optional, List, Dict, Any
from init_shared import shared_data
from logger import Logger
logger = Logger(name="comment.py", level=20) # INFO
# --- Helpers -----------------------------------------------------------------
class _SafeDict(dict):
"""Safe formatter: leaves unknown {placeholders} intact instead of raising."""
def __missing__(self, key):
return "{" + key + "}"
def _row_get(row: Any, key: str, default=None):
"""Safe accessor for rows that may be dict-like or sqlite3.Row."""
try:
return row.get(key, default)
except Exception:
try:
return row[key]
except Exception:
return default
# --- Main class --------------------------------------------------------------
class CommentAI:
"""
AI-style comment generator for status messages with:
- Randomized delay between messages
- Database-backed phrases (text, status, theme, lang, weight)
- Multilingual search with language priority and fallbacks
- Safe string templates: "Trying {user}@{ip}..."
"""
def __init__(self):
self.shared_data = shared_data
# Timing configuration with robust defaults
self.delay_min = max(1, int(getattr(self.shared_data, "comment_delaymin", 5)))
self.delay_max = max(self.delay_min, int(getattr(self.shared_data, "comment_delaymax", 15)))
self.comment_delay = self._new_delay()
# State tracking
self.last_comment_time: float = 0.0
self.last_status: Optional[str] = None
# Ensure comments are loaded in database
self._ensure_comments_loaded()
# Initialize first comment for UI using language priority
if not hasattr(self.shared_data, "bjorn_says") or not getattr(self.shared_data, "bjorn_says"):
first = self._pick_text("IDLE", lang=None, params=None)
self.shared_data.bjorn_says = first or "Initializing..."
# --- Language priority & JSON discovery ----------------------------------
def _lang_priority(self, preferred: Optional[str] = None) -> List[str]:
"""
Build ordered language preference list, deduplicated.
Priority sources:
1. explicit `preferred`
2. shared_data.lang_priority (list)
3. shared_data.lang (single fallback)
4. defaults ["en", "fr"]
"""
order: List[str] = []
def norm(x: Optional[str]) -> Optional[str]:
if not x:
return None
x = str(x).strip().lower()
return x[:2] if x else None
# 1) explicit override
p = norm(preferred)
if p:
order.append(p)
sd = self.shared_data
# 2) list from shared_data
if hasattr(sd, "lang_priority") and isinstance(sd.lang_priority, (list, tuple)):
order += [l for l in (norm(x) for x in sd.lang_priority) if l]
# 3) single language from shared_data
if hasattr(sd, "lang"):
l = norm(sd.lang)
if l:
order.append(l)
# 4) fallback defaults
order += ["en", "fr"]
# Deduplicate while preserving order
seen, res = set(), []
for l in order:
if l and l not in seen:
seen.add(l)
res.append(l)
return res
def _get_comments_json_paths(self, lang: Optional[str] = None) -> List[str]:
"""
Return candidate JSON paths, restricted to default_comments_dir (and explicit comments_file).
Supported patterns:
- {comments_file} (explicit)
- {default_comments_dir}/comments.json
- {default_comments_dir}/comments.<lang>.json
- {default_comments_dir}/{lang}/comments.json
"""
lang = (lang or "").strip().lower()
candidates = []
# 1) Explicit path from shared_data
comments_file = getattr(self.shared_data, "comments_file", "") or ""
if comments_file:
candidates.append(comments_file)
# 2) Default comments directory
default_dir = getattr(self.shared_data, "default_comments_dir", "")
if default_dir:
candidates += [
os.path.join(default_dir, "comments.json"),
os.path.join(default_dir, f"comments.{lang}.json") if lang else "",
os.path.join(default_dir, lang, "comments.json") if lang else "",
]
# Deduplicate
unique_paths, seen = [], set()
for p in candidates:
p = (p or "").strip()
if p and p not in seen:
seen.add(p)
unique_paths.append(p)
return unique_paths
# --- Bootstrapping DB -----------------------------------------------------
def _ensure_comments_loaded(self):
"""Ensure comments are present in DB; import JSON if empty."""
try:
comment_count = int(self.shared_data.db.count_comments())
except Exception as e:
logger.error(f"Database error counting comments: {e}")
comment_count = 0
if comment_count > 0:
logger.debug(f"Comments already in database: {comment_count}")
return
imported = 0
for lang in self._lang_priority():
for json_path in self._get_comments_json_paths(lang):
if os.path.exists(json_path):
try:
count = int(self.shared_data.db.import_comments_from_json(json_path))
imported += count
if count > 0:
logger.info(f"Imported {count} comments (auto-detected lang) from {json_path}")
break # stop at first successful import
except Exception as e:
logger.error(f"Failed to import comments from {json_path}: {e}")
if imported > 0:
break
if imported == 0:
logger.debug("No comments imported, seeding minimal fallback set")
self._seed_minimal_comments()
def _seed_minimal_comments(self):
"""
Seed minimal set when no JSON available.
Schema per row: (text, status, theme, lang, weight)
"""
default_comments = [
# English
("Scanning network for targets...", "NetworkScanner", "NetworkScanner", "en", 2),
("System idle, awaiting commands.", "IDLE", "IDLE", "en", 3),
("Analyzing network topology...", "NetworkScanner", "NetworkScanner", "en", 1),
("Processing authentication attempts...", "SSHBruteforce", "SSHBruteforce", "en", 2),
("Searching for vulnerabilities...", "NmapVulnScanner", "NmapVulnScanner", "en", 2),
("Extracting credentials from services...", "CredExtractor", "CredExtractor", "en", 1),
("Monitoring network changes...", "IDLE", "IDLE", "en", 2),
("Ready for deployment.", "IDLE", "IDLE", "en", 1),
("Target acquisition in progress...", "NetworkScanner", "NetworkScanner", "en", 1),
("Establishing secure connections...", "SSHBruteforce", "SSHBruteforce", "en", 1),
# French (bonus minimal)
("Analyse du réseau en cours...", "NetworkScanner", "NetworkScanner", "fr", 2),
("Système au repos, en attente dordres.", "IDLE", "IDLE", "fr", 3),
("Cartographie de la topologie réseau...", "NetworkScanner", "NetworkScanner", "fr", 1),
("Tentatives dauthentification en cours...", "SSHBruteforce", "SSHBruteforce", "fr", 2),
("Recherche de vulnérabilités...", "NmapVulnScanner", "NmapVulnScanner", "fr", 2),
("Extraction didentifiants depuis les services...", "CredExtractor", "CredExtractor", "fr", 1),
]
try:
self.shared_data.db.insert_comments(default_comments)
logger.info(f"Seeded {len(default_comments)} minimal comments into database")
except Exception as e:
logger.error(f"Failed to seed minimal comments: {e}")
# --- Core selection -------------------------------------------------------
def _new_delay(self) -> int:
"""Generate new random delay between comments."""
delay = random.randint(self.delay_min, self.delay_max)
logger.debug(f"Next comment delay: {delay}s")
return delay
def _pick_text(
self,
status: str,
lang: Optional[str],
params: Optional[Dict[str, Any]] = None
) -> Optional[str]:
"""
Pick a weighted comment across language preference; supports {templates}.
Selection cascade (per language in priority order):
1) (lang, status)
2) (lang, 'ANY')
3) (lang, 'IDLE')
Then cross-language:
4) (any, status)
5) (any, 'IDLE')
"""
status = status or "IDLE"
langs = self._lang_priority(preferred=lang)
# Language-scoped queries
rows = []
queries = [
("SELECT text, weight FROM comments WHERE lang=? AND status=?", lambda L: (L, status)),
("SELECT text, weight FROM comments WHERE lang=? AND status='ANY'", lambda L: (L,)),
("SELECT text, weight FROM comments WHERE lang=? AND status='IDLE'", lambda L: (L,)),
]
for L in langs:
for sql, args_fn in queries:
try:
rows = self.shared_data.db.query(sql, args_fn(L))
except Exception as e:
logger.error(f"DB query failed: {e}")
rows = []
if rows:
break
if rows:
break
# Cross-language fallbacks
if not rows:
for sql, args in [
("SELECT text, weight FROM comments WHERE status=? ORDER BY RANDOM() LIMIT 50", (status,)),
("SELECT text, weight FROM comments WHERE status='IDLE' ORDER BY RANDOM() LIMIT 50", ()),
]:
try:
rows = self.shared_data.db.query(sql, args)
except Exception as e:
logger.error(f"DB query failed: {e}")
rows = []
if rows:
break
if not rows:
return None
# Weighted selection pool
pool: List[str] = []
for row in rows:
try:
w = int(_row_get(row, "weight", 1)) or 1
except Exception:
w = 1
w = max(1, w)
text = _row_get(row, "text", "")
if text:
pool.extend([text] * w)
chosen = random.choice(pool) if pool else _row_get(rows[0], "text", None)
# Templates {var}
if chosen and params:
try:
chosen = str(chosen).format_map(_SafeDict(params))
except Exception:
# Keep the raw text if formatting fails
pass
return chosen
# --- Public API -----------------------------------------------------------
def get_comment(
self,
status: str,
lang: Optional[str] = None,
params: Optional[Dict[str, Any]] = None
) -> Optional[str]:
"""
Return a comment if status changed or delay expired.
Args:
status: logical status name (e.g., "IDLE", "SSHBruteforce", "NetworkScanner").
lang: language override (e.g., "fr"); if None, auto priority is used.
params: optional dict to format templates with {placeholders}.
Returns:
str or None: A new comment, or None if not time yet and status unchanged.
"""
current_time = time.time()
status = status or "IDLE"
status_changed = (status != self.last_status)
if status_changed or (current_time - self.last_comment_time >= self.comment_delay):
text = self._pick_text(status, lang, params)
if text:
self.last_status = status
self.last_comment_time = current_time
self.comment_delay = self._new_delay()
logger.debug(f"Next comment delay: {self.comment_delay}s")
return text
return None
# Backward compatibility alias
Commentaireia = CommentAI