feat: implement Bounty Vault for captured credentials and artifacts
This commit is contained in:
@@ -135,6 +135,13 @@ class LogsResponse(BaseModel):
|
||||
data: list[dict[str, Any]]
|
||||
|
||||
|
||||
class BountyResponse(BaseModel):
|
||||
total: int
|
||||
limit: int
|
||||
offset: int
|
||||
data: list[dict[str, Any]]
|
||||
|
||||
|
||||
@app.post("/api/v1/auth/login", response_model=Token)
|
||||
async def login(request: LoginRequest) -> dict[str, Any]:
|
||||
_user: Optional[dict[str, Any]] = await repo.get_user_by_username(request.username)
|
||||
@@ -190,6 +197,25 @@ async def get_logs(
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/v1/bounty", response_model=BountyResponse)
|
||||
async def get_bounties(
|
||||
limit: int = Query(50, ge=1, le=1000),
|
||||
offset: int = Query(0, ge=0),
|
||||
bounty_type: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
current_user: str = Depends(get_current_user)
|
||||
) -> dict[str, Any]:
|
||||
"""Retrieve collected bounties (harvested credentials, payloads, etc.)."""
|
||||
_data = await repo.get_bounties(limit=limit, offset=offset, bounty_type=bounty_type, search=search)
|
||||
_total = await repo.get_total_bounties(bounty_type=bounty_type, search=search)
|
||||
return {
|
||||
"total": _total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"data": _data
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/v1/logs/histogram")
|
||||
async def get_logs_histogram(
|
||||
search: Optional[str] = None,
|
||||
|
||||
@@ -54,6 +54,7 @@ async def log_ingestion_worker(repo: BaseRepository) -> None:
|
||||
try:
|
||||
_log_data: dict[str, Any] = json.loads(_line.strip())
|
||||
await repo.add_log(_log_data)
|
||||
await _extract_bounty(repo, _log_data)
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"Failed to decode JSON log line: {_line}")
|
||||
continue
|
||||
@@ -66,3 +67,28 @@ async def log_ingestion_worker(repo: BaseRepository) -> None:
|
||||
await asyncio.sleep(5)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
async def _extract_bounty(repo: BaseRepository, log_data: dict[str, Any]) -> None:
|
||||
"""Detect and extract valuable artifacts (bounties) from log entries."""
|
||||
_fields = log_data.get("fields")
|
||||
if not isinstance(_fields, dict):
|
||||
return
|
||||
|
||||
# 1. Credentials (User/Pass)
|
||||
_user = _fields.get("username")
|
||||
_pass = _fields.get("password")
|
||||
|
||||
if _user and _pass:
|
||||
await repo.add_bounty({
|
||||
"decky": log_data.get("decky"),
|
||||
"service": log_data.get("service"),
|
||||
"attacker_ip": log_data.get("attacker_ip"),
|
||||
"bounty_type": "credential",
|
||||
"payload": {
|
||||
"username": _user,
|
||||
"password": _pass
|
||||
}
|
||||
})
|
||||
|
||||
# 2. Add more extractors here later (e.g. file hashes, crypto keys)
|
||||
|
||||
@@ -59,3 +59,24 @@ class BaseRepository(ABC):
|
||||
async def update_user_password(self, uuid: str, password_hash: str, must_change_password: bool = False) -> None:
|
||||
"""Update a user's password and change the must_change_password flag."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def add_bounty(self, bounty_data: dict[str, Any]) -> None:
|
||||
"""Add a new harvested artifact (bounty) to the database."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_bounties(
|
||||
self,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
bounty_type: Optional[str] = None,
|
||||
search: Optional[str] = None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Retrieve paginated bounty entries."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_total_bounties(self, bounty_type: Optional[str] = None, search: Optional[str] = None) -> int:
|
||||
"""Retrieve the total count of bounties, optionally filtered."""
|
||||
pass
|
||||
|
||||
@@ -37,6 +37,17 @@ class SQLiteRepository(BaseRepository):
|
||||
must_change_password BOOLEAN DEFAULT 0
|
||||
)
|
||||
""")
|
||||
_conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS bounty (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
decky TEXT,
|
||||
service TEXT,
|
||||
attacker_ip TEXT,
|
||||
bounty_type TEXT,
|
||||
payload TEXT
|
||||
)
|
||||
""")
|
||||
_conn.commit()
|
||||
|
||||
async def add_log(self, log_data: dict[str, Any]) -> None:
|
||||
@@ -296,3 +307,75 @@ class SQLiteRepository(BaseRepository):
|
||||
(password_hash, must_change_password, uuid)
|
||||
)
|
||||
await _db.commit()
|
||||
|
||||
async def add_bounty(self, bounty_data: dict[str, Any]) -> None:
|
||||
import json
|
||||
async with aiosqlite.connect(self.db_path) as _db:
|
||||
await _db.execute(
|
||||
"INSERT INTO bounty (decky, service, attacker_ip, bounty_type, payload) VALUES (?, ?, ?, ?, ?)",
|
||||
(
|
||||
bounty_data.get("decky"),
|
||||
bounty_data.get("service"),
|
||||
bounty_data.get("attacker_ip"),
|
||||
bounty_data.get("bounty_type"),
|
||||
json.dumps(bounty_data.get("payload", {}))
|
||||
)
|
||||
)
|
||||
await _db.commit()
|
||||
|
||||
def _build_bounty_where(
|
||||
self,
|
||||
bounty_type: Optional[str] = None,
|
||||
search: Optional[str] = None
|
||||
) -> tuple[str, list[Any]]:
|
||||
_where_clauses = []
|
||||
_params = []
|
||||
|
||||
if bounty_type:
|
||||
_where_clauses.append("bounty_type = ?")
|
||||
_params.append(bounty_type)
|
||||
|
||||
if search:
|
||||
_where_clauses.append("(decky LIKE ? OR service LIKE ? OR attacker_ip LIKE ? OR payload LIKE ?)")
|
||||
_like_val = f"%{search}%"
|
||||
_params.extend([_like_val, _like_val, _like_val, _like_val])
|
||||
|
||||
if _where_clauses:
|
||||
return " WHERE " + " AND ".join(_where_clauses), _params
|
||||
return "", []
|
||||
|
||||
async def get_bounties(
|
||||
self,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
bounty_type: Optional[str] = None,
|
||||
search: Optional[str] = None
|
||||
) -> list[dict[str, Any]]:
|
||||
import json
|
||||
_where, _params = self._build_bounty_where(bounty_type, search)
|
||||
_query = f"SELECT * FROM bounty{_where} ORDER BY timestamp DESC LIMIT ? OFFSET ?" # nosec B608
|
||||
_params.extend([limit, offset])
|
||||
|
||||
async with aiosqlite.connect(self.db_path) as _db:
|
||||
_db.row_factory = aiosqlite.Row
|
||||
async with _db.execute(_query, _params) as _cursor:
|
||||
_rows: list[aiosqlite.Row] = await _cursor.fetchall()
|
||||
_results = []
|
||||
for _row in _rows:
|
||||
_d = dict(_row)
|
||||
try:
|
||||
_d["payload"] = json.loads(_d["payload"])
|
||||
except Exception:
|
||||
pass
|
||||
_results.append(_d)
|
||||
return _results
|
||||
|
||||
async def get_total_bounties(self, bounty_type: Optional[str] = None, search: Optional[str] = None) -> int:
|
||||
_where, _params = self._build_bounty_where(bounty_type, search)
|
||||
_query = f"SELECT COUNT(*) as total FROM bounty{_where}" # nosec B608
|
||||
|
||||
async with aiosqlite.connect(self.db_path) as _db:
|
||||
_db.row_factory = aiosqlite.Row
|
||||
async with _db.execute(_query, _params) as _cursor:
|
||||
_row: Optional[aiosqlite.Row] = await _cursor.fetchone()
|
||||
return _row["total"] if _row else 0
|
||||
|
||||
Reference in New Issue
Block a user