main.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  1. from __future__ import annotations
  2. import json
  3. import sqlite3
  4. from typing import Any
  5. from fastapi import FastAPI, HTTPException, Query
  6. from fastapi.middleware.cors import CORSMiddleware
  7. from .control import (
  8. CONFIRMED_CONTROL_STATUSES,
  9. restart_service,
  10. start_process,
  11. start_service,
  12. stop_process,
  13. stop_service,
  14. )
  15. from .database import get_db, init_db
  16. from .scanner import now_iso, run_full_scan
  17. from .sensors import collect_sensors
  18. from .schemas import AiImportRequest, BatchStatusUpdate, PromptRequest, StatusUpdate, TagAssignRequest, TagCreate, TagUpdate
  19. from .smart import collect_all_smart, get_device_smart, scan_devices
  20. AI_PROMPT_TEMPLATE = """请作为资深的 Windows 系统安全专家,帮我分析下面这些 Windows 服务和进程是否可信,并严格按照 JSON 数组格式输出结果。
  21. 输出要求:
  22. 1. 必须且只能输出纯 JSON 数组,不要输出任何额外的解释、问候语,也不要使用 Markdown 代码块(如 ```json)包裹。
  23. 2. 每个对象必须包含以下 7 个字段:type、name、description、judgement、risk_level、reason、suggestion。
  24. 3. type 只能是 "service" 或 "process"。
  25. 4. description 请简要说明该服务或进程的官方用途或常规功能(如果是未知/恶意程序,请描述其伪装意图或表现)。
  26. 5. judgement 只能是 "TRUSTED"、"SUSPICIOUS"、"NEED_MORE_INFO"。
  27. 6. risk_level 只能是 "LOW"、"MEDIUM"、"HIGH"。
  28. 7. 如果提供的信息不足以做出判断,请将 judgement 设为 "NEED_MORE_INFO"。
  29. 8. 请结合每个对象的 tags 字段进行判断。已有标签是人工上下文,不代表最终结论,但如果标签显示为“windows系统”或“本系统相关”,请在 reason 或 suggestion 中体现这一点。
  30. 9. 如果你认为某个对象适合系统已有标签,可以在 suggestion 中建议使用对应标签名称;不要创造不存在的新标签。
  31. JSON 格式示例:
  32. [
  33. {
  34. "type": "service",
  35. "name": "WinDefend",
  36. "description": "Microsoft Defender 防病毒核心服务,负责保护系统免受恶意软件和间谍软件的威胁。",
  37. "judgement": "TRUSTED",
  38. "risk_level": "LOW",
  39. "reason": "这是 Microsoft 官方的安全组件,路径和名称符合系统原生服务的标准特征。",
  40. "suggestion": "可标记为可信,建议保持运行。"
  41. },
  42. {
  43. "type": "process",
  44. "name": "unknown.exe",
  45. "description": "未知用途的执行文件,无明确的官方功能说明。",
  46. "judgement": "SUSPICIOUS",
  47. "risk_level": "HIGH",
  48. "reason": "进程位于用户 AppData 临时目录,启动命令行异常,且缺少有效的官方数字签名。",
  49. "suggestion": "建议立即隔离,检查文件的 SHA256 散列值及外部网络连接记录,不要直接运行或信任。"
  50. }
  51. ]
  52. 下面是待分析数据:
  53. {pending_items_json}
  54. 系统中已有标签信息:
  55. {tags_json}
  56. """
  57. app = FastAPI(title="Windows Monitor API", version="1.0.0")
  58. app.add_middleware(
  59. CORSMiddleware,
  60. allow_origins=["*"],
  61. allow_credentials=True,
  62. allow_methods=["*"],
  63. allow_headers=["*"],
  64. )
  65. @app.on_event("startup")
  66. def startup() -> None:
  67. init_db()
  68. def build_where(
  69. keyword: str | None,
  70. confirm_status: str | None,
  71. present: bool | None,
  72. fields: list[str],
  73. ) -> tuple[str, list[Any]]:
  74. clauses: list[str] = []
  75. params: list[Any] = []
  76. if keyword:
  77. like = f"%{keyword}%"
  78. clauses.append("(" + " OR ".join(f"{field} LIKE ?" for field in fields) + ")")
  79. params.extend([like] * len(fields))
  80. if confirm_status:
  81. clauses.append("confirm_status = ?")
  82. params.append(confirm_status)
  83. if present is not None:
  84. clauses.append("is_present_now = ?")
  85. params.append(1 if present else 0)
  86. return ("WHERE " + " AND ".join(clauses)) if clauses else "", params
  87. def list_items(
  88. table: str,
  89. keyword: str | None,
  90. confirm_status: str | None,
  91. present: bool | None,
  92. page: int,
  93. page_size: int,
  94. fields: list[str],
  95. sort_by: str | None = None,
  96. sort_order: str | None = None,
  97. ) -> dict[str, Any]:
  98. where_sql, params = build_where(keyword, confirm_status, present, fields)
  99. order_sql = build_order_by(table, sort_by, sort_order)
  100. offset = (page - 1) * page_size
  101. with get_db() as conn:
  102. total = conn.execute(f"SELECT COUNT(*) AS total FROM {table} {where_sql}", params).fetchone()["total"]
  103. rows = conn.execute(
  104. f"SELECT * FROM {table} {where_sql} {order_sql} LIMIT ? OFFSET ?",
  105. [*params, page_size, offset],
  106. ).fetchall()
  107. rows = attach_item_metadata(conn, table_to_item_type(table), rows)
  108. return {"items": rows, "total": total, "page": page, "page_size": page_size}
  109. def build_order_by(table: str, sort_by: str | None, sort_order: str | None) -> str:
  110. allowed = {
  111. "windows_services": {
  112. "name",
  113. "display_name",
  114. "status",
  115. "start_type",
  116. "username",
  117. "is_present_now",
  118. "confirm_status",
  119. "first_seen_at",
  120. "last_seen_at",
  121. "updated_at",
  122. },
  123. "windows_processes": {
  124. "name",
  125. "exe_path",
  126. "username",
  127. "status",
  128. "last_pid",
  129. "parent_pid",
  130. "is_present_now",
  131. "confirm_status",
  132. "create_time",
  133. "first_seen_at",
  134. "last_seen_at",
  135. "updated_at",
  136. },
  137. }
  138. default_sql = "ORDER BY is_present_now DESC, last_seen_at DESC"
  139. if not sort_by or sort_by not in allowed.get(table, set()):
  140. return default_sql
  141. direction = "ASC" if sort_order == "asc" else "DESC"
  142. if sort_by == "is_present_now":
  143. return f"ORDER BY {sort_by} {direction}, last_seen_at DESC"
  144. return f"ORDER BY {sort_by} {direction}, is_present_now DESC, last_seen_at DESC"
  145. def get_item(table: str, item_id: int) -> dict[str, Any]:
  146. with get_db() as conn:
  147. item = conn.execute(f"SELECT * FROM {table} WHERE id = ?", (item_id,)).fetchone()
  148. if item:
  149. item = attach_item_metadata(conn, table_to_item_type(table), [item])[0]
  150. if not item:
  151. raise HTTPException(status_code=404, detail="Item not found")
  152. return item
  153. def table_to_item_type(table: str) -> str:
  154. if table == "windows_services":
  155. return "service"
  156. if table == "windows_processes":
  157. return "process"
  158. raise ValueError(f"Unsupported table: {table}")
  159. def bool_tag(row: dict[str, Any]) -> dict[str, Any]:
  160. item = dict(row)
  161. item["is_controllable"] = bool(item["is_controllable"])
  162. item["is_builtin"] = bool(item["is_builtin"])
  163. return item
  164. def all_tags(conn) -> list[dict[str, Any]]:
  165. return [
  166. bool_tag(row)
  167. for row in conn.execute("SELECT * FROM tags ORDER BY is_builtin DESC, name ASC").fetchall()
  168. ]
  169. def tags_for_items(conn, item_type: str, item_ids: list[int]) -> dict[int, list[dict[str, Any]]]:
  170. if not item_ids:
  171. return {}
  172. placeholders = ",".join("?" for _ in item_ids)
  173. rows = conn.execute(
  174. f"""
  175. SELECT it.item_id, t.*
  176. FROM item_tags it
  177. JOIN tags t ON t.id = it.tag_id
  178. WHERE it.item_type = ? AND it.item_id IN ({placeholders})
  179. ORDER BY t.name ASC
  180. """,
  181. [item_type, *item_ids],
  182. ).fetchall()
  183. result = {item_id: [] for item_id in item_ids}
  184. for row in rows:
  185. item_id = row["item_id"]
  186. tag = {key: value for key, value in row.items() if key != "item_id"}
  187. result.setdefault(item_id, []).append(bool_tag(tag))
  188. return result
  189. def can_control_item(item_type: str, row: dict[str, Any], tags: list[dict[str, Any]]) -> bool:
  190. if row.get("confirm_status") not in CONFIRMED_CONTROL_STATUSES:
  191. return False
  192. if item_type == "process":
  193. protected_names = {"system idle process", "system", "registry"}
  194. if row.get("last_pid") in (0, 4) or (row.get("name") or "").lower() in protected_names:
  195. return False
  196. return all(tag.get("is_controllable", True) for tag in tags)
  197. def attach_item_metadata(conn, item_type: str, rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
  198. tag_map = tags_for_items(conn, item_type, [row["id"] for row in rows])
  199. enriched = []
  200. for row in rows:
  201. item = dict(row)
  202. item["is_present_now"] = bool(item.get("is_present_now"))
  203. item["tags"] = tag_map.get(row["id"], [])
  204. item["can_control"] = can_control_item(item_type, item, item["tags"])
  205. enriched.append(item)
  206. return enriched
  207. def update_one(table: str, item_id: int, payload: StatusUpdate) -> dict[str, Any]:
  208. get_item(table, item_id)
  209. with get_db() as conn:
  210. conn.execute(
  211. f"UPDATE {table} SET confirm_status = ?, user_note = ?, updated_at = ? WHERE id = ?",
  212. (payload.confirm_status, payload.user_note, now_iso(), item_id),
  213. )
  214. return get_item(table, item_id)
  215. def update_batch(table: str, payload: BatchStatusUpdate) -> dict[str, Any]:
  216. if not payload.ids:
  217. return {"updated": 0}
  218. placeholders = ",".join("?" for _ in payload.ids)
  219. with get_db() as conn:
  220. cursor = conn.execute(
  221. f"""
  222. UPDATE {table}
  223. SET confirm_status = ?, user_note = COALESCE(?, user_note), updated_at = ?
  224. WHERE id IN ({placeholders})
  225. """,
  226. [payload.confirm_status, payload.user_note, now_iso(), *payload.ids],
  227. )
  228. return {"updated": cursor.rowcount}
  229. def rows_for_prompt(table: str, item_type: str, payload: PromptRequest) -> list[dict[str, Any]]:
  230. with get_db() as conn:
  231. if payload.scope == "selected" and payload.ids:
  232. placeholders = ",".join("?" for _ in payload.ids)
  233. rows = conn.execute(f"SELECT * FROM {table} WHERE id IN ({placeholders})", payload.ids).fetchall()
  234. else:
  235. rows = conn.execute(f"SELECT * FROM {table} WHERE confirm_status = 'PENDING'").fetchall()
  236. rows = attach_item_metadata(conn, item_type, rows)
  237. return [normalize_prompt_row(item_type, row) for row in rows]
  238. def normalize_prompt_row(item_type: str, row: dict[str, Any]) -> dict[str, Any]:
  239. if item_type == "service":
  240. return {
  241. "type": "service",
  242. "id": row["id"],
  243. "name": row["name"],
  244. "display_name": row["display_name"],
  245. "status": row["status"],
  246. "start_type": row["start_type"],
  247. "username": row["username"],
  248. "binary_path": row["binary_path"],
  249. "description": row["description"],
  250. "is_present_now": bool(row["is_present_now"]),
  251. "tags": [
  252. {
  253. "name": tag["name"],
  254. "description": tag["description"],
  255. "is_controllable": tag["is_controllable"],
  256. }
  257. for tag in row.get("tags", [])
  258. ],
  259. }
  260. return {
  261. "type": "process",
  262. "id": row["id"],
  263. "name": row["name"],
  264. "exe_path": row["exe_path"],
  265. "cmdline": row["cmdline"],
  266. "username": row["username"],
  267. "status": row["status"],
  268. "last_pid": row["last_pid"],
  269. "parent_pid": row["parent_pid"],
  270. "is_present_now": bool(row["is_present_now"]),
  271. "tags": [
  272. {
  273. "name": tag["name"],
  274. "description": tag["description"],
  275. "is_controllable": tag["is_controllable"],
  276. }
  277. for tag in row.get("tags", [])
  278. ],
  279. }
  280. def markdown_table(rows: list[dict[str, Any]]) -> str:
  281. headers = ["type", "id", "name", "status", "tags", "path_or_command", "user", "present"]
  282. lines = ["| " + " | ".join(headers) + " |", "| " + " | ".join(["---"] * len(headers)) + " |"]
  283. for row in rows:
  284. path_or_command = row.get("binary_path") or row.get("exe_path") or row.get("cmdline") or ""
  285. values = [
  286. str(row.get("type", "")),
  287. str(row.get("id", "")),
  288. str(row.get("name", "")),
  289. str(row.get("status", "")),
  290. ", ".join(tag.get("name", "") for tag in row.get("tags", [])).replace("|", "\\|"),
  291. str(path_or_command).replace("|", "\\|"),
  292. str(row.get("username", "")).replace("|", "\\|"),
  293. "yes" if row.get("is_present_now") else "no",
  294. ]
  295. lines.append("| " + " | ".join(values) + " |")
  296. return "\n".join(lines)
  297. def prompt_response(rows: list[dict[str, Any]]) -> dict[str, Any]:
  298. pending_json = json.dumps(rows, ensure_ascii=False, indent=2)
  299. table = markdown_table(rows)
  300. with get_db() as conn:
  301. tags_json = json.dumps(all_tags(conn), ensure_ascii=False, indent=2)
  302. prompt_text = AI_PROMPT_TEMPLATE.replace("{pending_items_json}", pending_json).replace("{tags_json}", tags_json)
  303. return {"prompt_text": prompt_text, "markdown_table": table, "items": rows}
  304. def set_item_tags(item_type: str, table: str, item_id: int, payload: TagAssignRequest) -> dict[str, Any]:
  305. get_item(table, item_id)
  306. unique_ids = sorted(set(payload.tag_ids))
  307. now = now_iso()
  308. with get_db() as conn:
  309. if unique_ids:
  310. placeholders = ",".join("?" for _ in unique_ids)
  311. found = conn.execute(f"SELECT id FROM tags WHERE id IN ({placeholders})", unique_ids).fetchall()
  312. if len(found) != len(unique_ids):
  313. raise HTTPException(status_code=400, detail="One or more tag ids do not exist")
  314. conn.execute("DELETE FROM item_tags WHERE item_type = ? AND item_id = ?", (item_type, item_id))
  315. for tag_id in unique_ids:
  316. conn.execute(
  317. "INSERT INTO item_tags (item_type, item_id, tag_id, created_at) VALUES (?, ?, ?, ?)",
  318. (item_type, item_id, tag_id, now),
  319. )
  320. return get_item(table, item_id)
  321. def ensure_control_allowed(table: str, item_id: int) -> dict[str, Any]:
  322. item = get_item(table, item_id)
  323. if not item.get("can_control"):
  324. raise HTTPException(status_code=403, detail="This item is not controllable because it is unconfirmed or has a non-controllable tag")
  325. return item
  326. def import_ai_results(table: str, item_type: str, payload: AiImportRequest) -> dict[str, Any]:
  327. updated = 0
  328. with get_db() as conn:
  329. for item in payload.items:
  330. if item.type != item_type:
  331. continue
  332. cursor = conn.execute(
  333. f"""
  334. UPDATE {table}
  335. SET confirm_status = ?, ai_description = ?, ai_reason = ?,
  336. ai_suggestion = ?, risk_level = ?, updated_at = ?
  337. WHERE name = ?
  338. """,
  339. (
  340. item.judgement,
  341. item.description,
  342. item.reason,
  343. item.suggestion,
  344. item.risk_level,
  345. now_iso(),
  346. item.name,
  347. ),
  348. )
  349. updated += cursor.rowcount
  350. return {"updated": updated}
  351. @app.get("/api/dashboard")
  352. def dashboard() -> dict[str, Any]:
  353. with get_db() as conn:
  354. latest_scan = conn.execute("SELECT * FROM scan_records ORDER BY started_at DESC LIMIT 1").fetchone()
  355. service_total = conn.execute("SELECT COUNT(*) AS total FROM windows_services").fetchone()["total"]
  356. process_total = conn.execute("SELECT COUNT(*) AS total FROM windows_processes").fetchone()["total"]
  357. pending_services = conn.execute(
  358. "SELECT COUNT(*) AS total FROM windows_services WHERE confirm_status = 'PENDING'"
  359. ).fetchone()["total"]
  360. pending_processes = conn.execute(
  361. "SELECT COUNT(*) AS total FROM windows_processes WHERE confirm_status = 'PENDING'"
  362. ).fetchone()["total"]
  363. missing_services = conn.execute(
  364. "SELECT COUNT(*) AS total FROM windows_services WHERE is_present_now = 0"
  365. ).fetchone()["total"]
  366. missing_processes = conn.execute(
  367. "SELECT COUNT(*) AS total FROM windows_processes WHERE is_present_now = 0"
  368. ).fetchone()["total"]
  369. return {
  370. "latest_scan": latest_scan,
  371. "service_total": service_total,
  372. "process_total": process_total,
  373. "pending_services": pending_services,
  374. "pending_processes": pending_processes,
  375. "missing_services": missing_services,
  376. "missing_processes": missing_processes,
  377. }
  378. @app.get("/api/tags")
  379. def tags() -> dict[str, Any]:
  380. with get_db() as conn:
  381. rows = all_tags(conn)
  382. return {"items": rows}
  383. @app.post("/api/tags")
  384. def tag_create(payload: TagCreate) -> dict[str, Any]:
  385. now = now_iso()
  386. try:
  387. with get_db() as conn:
  388. cursor = conn.execute(
  389. """
  390. INSERT INTO tags (name, description, is_controllable, is_builtin, created_at, updated_at)
  391. VALUES (?, ?, ?, 0, ?, ?)
  392. """,
  393. (payload.name.strip(), payload.description, 1 if payload.is_controllable else 0, now, now),
  394. )
  395. tag_id = cursor.lastrowid
  396. row = conn.execute("SELECT * FROM tags WHERE id = ?", (tag_id,)).fetchone()
  397. except sqlite3.IntegrityError as exc:
  398. raise HTTPException(status_code=409, detail="Tag name already exists") from exc
  399. return bool_tag(row)
  400. @app.patch("/api/tags/{tag_id}")
  401. def tag_update(tag_id: int, payload: TagUpdate) -> dict[str, Any]:
  402. now = now_iso()
  403. try:
  404. with get_db() as conn:
  405. existing = conn.execute("SELECT * FROM tags WHERE id = ?", (tag_id,)).fetchone()
  406. if not existing:
  407. raise HTTPException(status_code=404, detail="Tag not found")
  408. conn.execute(
  409. """
  410. UPDATE tags
  411. SET name = ?, description = ?, is_controllable = ?, updated_at = ?
  412. WHERE id = ?
  413. """,
  414. (payload.name.strip(), payload.description, 1 if payload.is_controllable else 0, now, tag_id),
  415. )
  416. row = conn.execute("SELECT * FROM tags WHERE id = ?", (tag_id,)).fetchone()
  417. except sqlite3.IntegrityError as exc:
  418. raise HTTPException(status_code=409, detail="Tag name already exists") from exc
  419. return bool_tag(row)
  420. @app.delete("/api/tags/{tag_id}")
  421. def tag_delete(tag_id: int) -> dict[str, Any]:
  422. with get_db() as conn:
  423. row = conn.execute("SELECT * FROM tags WHERE id = ?", (tag_id,)).fetchone()
  424. if not row:
  425. raise HTTPException(status_code=404, detail="Tag not found")
  426. if row["is_builtin"]:
  427. raise HTTPException(status_code=400, detail="Built-in tags cannot be deleted")
  428. cursor = conn.execute("DELETE FROM tags WHERE id = ?", (tag_id,))
  429. return {"deleted": cursor.rowcount}
  430. @app.get("/api/sensors")
  431. def sensors() -> dict[str, Any]:
  432. return collect_sensors()
  433. @app.get("/api/smart/scan")
  434. def smart_scan() -> dict[str, Any]:
  435. return scan_devices()
  436. @app.get("/api/smart/devices")
  437. def smart_devices(include_jmb39x: bool = True, jmb39x_slots: int = Query(default=8, ge=0, le=16)) -> dict[str, Any]:
  438. return collect_all_smart(include_jmb39x=include_jmb39x, jmb39x_slots=jmb39x_slots)
  439. @app.get("/api/smart/device")
  440. def smart_device(device: str, device_type: str | None = None) -> dict[str, Any]:
  441. return get_device_smart(device, device_type)
  442. @app.post("/api/scans/run")
  443. def run_scan() -> dict[str, Any]:
  444. return run_full_scan()
  445. @app.get("/api/scans")
  446. def scan_history(page: int = 1, page_size: int = 20) -> dict[str, Any]:
  447. offset = (page - 1) * page_size
  448. with get_db() as conn:
  449. total = conn.execute("SELECT COUNT(*) AS total FROM scan_records").fetchone()["total"]
  450. rows = conn.execute(
  451. "SELECT * FROM scan_records ORDER BY started_at DESC LIMIT ? OFFSET ?",
  452. (page_size, offset),
  453. ).fetchall()
  454. return {"items": rows, "total": total, "page": page, "page_size": page_size}
  455. @app.get("/api/scans/{scan_id}")
  456. def scan_detail(scan_id: int) -> dict[str, Any]:
  457. with get_db() as conn:
  458. scan = conn.execute("SELECT * FROM scan_records WHERE id = ?", (scan_id,)).fetchone()
  459. if not scan:
  460. raise HTTPException(status_code=404, detail="Scan not found")
  461. return scan
  462. @app.get("/api/services")
  463. def services(
  464. keyword: str | None = None,
  465. confirm_status: str | None = None,
  466. present: bool | None = None,
  467. sort_by: str | None = None,
  468. sort_order: str | None = Query(default=None, pattern="^(asc|desc)$"),
  469. page: int = Query(default=1, ge=1),
  470. page_size: int = Query(default=20, ge=1, le=200),
  471. ) -> dict[str, Any]:
  472. return list_items(
  473. "windows_services",
  474. keyword,
  475. confirm_status,
  476. present,
  477. page,
  478. page_size,
  479. ["name", "display_name", "binary_path", "description"],
  480. sort_by,
  481. sort_order,
  482. )
  483. @app.patch("/api/services/batch")
  484. def service_batch_update(payload: BatchStatusUpdate) -> dict[str, Any]:
  485. return update_batch("windows_services", payload)
  486. @app.post("/api/services/import-ai")
  487. def service_import_ai(payload: AiImportRequest) -> dict[str, Any]:
  488. return import_ai_results("windows_services", "service", payload)
  489. @app.post("/api/services/ai-prompt")
  490. def service_ai_prompt(payload: PromptRequest) -> dict[str, Any]:
  491. return prompt_response(rows_for_prompt("windows_services", "service", payload))
  492. @app.put("/api/services/{service_id}/tags")
  493. def service_tags_update(service_id: int, payload: TagAssignRequest) -> dict[str, Any]:
  494. return set_item_tags("service", "windows_services", service_id, payload)
  495. @app.post("/api/services/{service_id}/start")
  496. def service_start(service_id: int) -> dict[str, Any]:
  497. item = ensure_control_allowed("windows_services", service_id)
  498. if not item.get("is_present_now"):
  499. raise HTTPException(status_code=400, detail="This service was not present in the latest scan")
  500. result = start_service(item["name"])
  501. with get_db() as conn:
  502. conn.execute(
  503. "UPDATE windows_services SET status = ?, updated_at = ? WHERE id = ?",
  504. (result.get("status") or "running", now_iso(), service_id),
  505. )
  506. return result
  507. @app.post("/api/services/{service_id}/stop")
  508. def service_stop(service_id: int) -> dict[str, Any]:
  509. item = ensure_control_allowed("windows_services", service_id)
  510. if not item.get("is_present_now"):
  511. raise HTTPException(status_code=400, detail="This service was not present in the latest scan")
  512. result = stop_service(item["name"])
  513. with get_db() as conn:
  514. conn.execute(
  515. "UPDATE windows_services SET status = ?, updated_at = ? WHERE id = ?",
  516. (result.get("status") or "stopped", now_iso(), service_id),
  517. )
  518. return result
  519. @app.post("/api/services/{service_id}/restart")
  520. def service_restart(service_id: int) -> dict[str, Any]:
  521. item = ensure_control_allowed("windows_services", service_id)
  522. if not item.get("is_present_now"):
  523. raise HTTPException(status_code=400, detail="This service was not present in the latest scan")
  524. result = restart_service(item["name"])
  525. with get_db() as conn:
  526. conn.execute(
  527. "UPDATE windows_services SET status = ?, updated_at = ? WHERE id = ?",
  528. (result.get("status") or "running", now_iso(), service_id),
  529. )
  530. return result
  531. @app.get("/api/services/{service_id}")
  532. def service_detail(service_id: int) -> dict[str, Any]:
  533. return get_item("windows_services", service_id)
  534. @app.patch("/api/services/{service_id}")
  535. def service_update(service_id: int, payload: StatusUpdate) -> dict[str, Any]:
  536. return update_one("windows_services", service_id, payload)
  537. @app.get("/api/processes")
  538. def processes(
  539. keyword: str | None = None,
  540. confirm_status: str | None = None,
  541. present: bool | None = None,
  542. sort_by: str | None = None,
  543. sort_order: str | None = Query(default=None, pattern="^(asc|desc)$"),
  544. page: int = Query(default=1, ge=1),
  545. page_size: int = Query(default=20, ge=1, le=200),
  546. ) -> dict[str, Any]:
  547. return list_items(
  548. "windows_processes",
  549. keyword,
  550. confirm_status,
  551. present,
  552. page,
  553. page_size,
  554. ["name", "exe_path", "cmdline", "username"],
  555. sort_by,
  556. sort_order,
  557. )
  558. @app.patch("/api/processes/batch")
  559. def process_batch_update(payload: BatchStatusUpdate) -> dict[str, Any]:
  560. return update_batch("windows_processes", payload)
  561. @app.post("/api/processes/import-ai")
  562. def process_import_ai(payload: AiImportRequest) -> dict[str, Any]:
  563. return import_ai_results("windows_processes", "process", payload)
  564. @app.post("/api/processes/ai-prompt")
  565. def process_ai_prompt(payload: PromptRequest) -> dict[str, Any]:
  566. return prompt_response(rows_for_prompt("windows_processes", "process", payload))
  567. @app.put("/api/processes/{process_id}/tags")
  568. def process_tags_update(process_id: int, payload: TagAssignRequest) -> dict[str, Any]:
  569. return set_item_tags("process", "windows_processes", process_id, payload)
  570. @app.post("/api/processes/{process_id}/start")
  571. def process_start(process_id: int) -> dict[str, Any]:
  572. item = ensure_control_allowed("windows_processes", process_id)
  573. result = start_process(item)
  574. with get_db() as conn:
  575. conn.execute(
  576. "UPDATE windows_processes SET last_pid = ?, is_present_now = 1, status = ?, updated_at = ? WHERE id = ?",
  577. (result.get("pid"), "running", now_iso(), process_id),
  578. )
  579. return result
  580. @app.post("/api/processes/{process_id}/stop")
  581. def process_stop(process_id: int) -> dict[str, Any]:
  582. item = ensure_control_allowed("windows_processes", process_id)
  583. if not item.get("is_present_now"):
  584. raise HTTPException(status_code=400, detail="This process was not present in the latest scan")
  585. result = stop_process(item)
  586. with get_db() as conn:
  587. conn.execute(
  588. "UPDATE windows_processes SET is_present_now = 0, status = ?, updated_at = ? WHERE id = ?",
  589. ("stopped", now_iso(), process_id),
  590. )
  591. return result
  592. @app.get("/api/processes/{process_id}")
  593. def process_detail(process_id: int) -> dict[str, Any]:
  594. return get_item("windows_processes", process_id)
  595. @app.patch("/api/processes/{process_id}")
  596. def process_update(process_id: int, payload: StatusUpdate) -> dict[str, Any]:
  597. return update_one("windows_processes", process_id, payload)