164 lines
5.6 KiB
Python
164 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
||
import json
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
from PyQt5 import QtCore, QtWidgets
|
||
|
||
|
||
APP_TITLE = "SAIKYO OS — Security Profile Manager"
|
||
|
||
|
||
def _run(cmd, check=False):
|
||
return subprocess.run(cmd, check=check)
|
||
|
||
|
||
def _pkexec(cmd):
|
||
# pkexec will prompt for admin auth via polkit
|
||
return _run(["/usr/bin/pkexec"] + cmd, check=False)
|
||
|
||
|
||
def _profiles_dir() -> Path:
|
||
return Path("/usr/share/saikyo-security-profile/profiles")
|
||
|
||
|
||
def _read_profile(profile: str) -> dict:
|
||
p = _profiles_dir() / f"{profile}.json"
|
||
return json.loads(p.read_text(encoding="utf-8"))
|
||
|
||
|
||
class MainWindow(QtWidgets.QMainWindow):
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.setWindowTitle(APP_TITLE)
|
||
self.setMinimumSize(720, 480)
|
||
|
||
root = QtWidgets.QWidget(self)
|
||
self.setCentralWidget(root)
|
||
|
||
self.profile_combo = QtWidgets.QComboBox()
|
||
self.profile_combo.addItems(["standard", "secure"])
|
||
|
||
self.apply_btn = QtWidgets.QPushButton("Применить")
|
||
self.export_btn = QtWidgets.QPushButton("Экспорт...")
|
||
self.import_btn = QtWidgets.QPushButton("Импорт...")
|
||
self.reload_btn = QtWidgets.QPushButton("Обновить")
|
||
|
||
self.preview = QtWidgets.QPlainTextEdit()
|
||
self.preview.setReadOnly(True)
|
||
self.preview.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap)
|
||
|
||
top = QtWidgets.QHBoxLayout()
|
||
top.addWidget(QtWidgets.QLabel("Профиль:"))
|
||
top.addWidget(self.profile_combo, 1)
|
||
top.addWidget(self.apply_btn)
|
||
top.addWidget(self.export_btn)
|
||
top.addWidget(self.import_btn)
|
||
top.addWidget(self.reload_btn)
|
||
|
||
layout = QtWidgets.QVBoxLayout(root)
|
||
layout.addLayout(top)
|
||
layout.addWidget(self.preview, 1)
|
||
|
||
self.apply_btn.clicked.connect(self.on_apply)
|
||
self.export_btn.clicked.connect(self.on_export)
|
||
self.import_btn.clicked.connect(self.on_import)
|
||
self.reload_btn.clicked.connect(self.refresh_preview)
|
||
self.profile_combo.currentTextChanged.connect(lambda _: self.refresh_preview())
|
||
|
||
self.refresh_preview()
|
||
|
||
def current_profile(self) -> str:
|
||
return self.profile_combo.currentText().strip()
|
||
|
||
def refresh_preview(self):
|
||
prof_name = self.current_profile()
|
||
try:
|
||
data = _read_profile(prof_name)
|
||
self.preview.setPlainText(json.dumps(data, indent=2, sort_keys=True) + "\n")
|
||
except Exception as e:
|
||
self.preview.setPlainText(f"Ошибка чтения профиля '{prof_name}': {e}\n")
|
||
|
||
def _msg(self, title: str, text: str, icon=QtWidgets.QMessageBox.Information):
|
||
m = QtWidgets.QMessageBox(self)
|
||
m.setIcon(icon)
|
||
m.setWindowTitle(title)
|
||
m.setText(text)
|
||
m.exec_()
|
||
|
||
def on_apply(self):
|
||
prof = self.current_profile()
|
||
rc = _pkexec(["/usr/sbin/saikyo-security-profile", "apply", prof]).returncode
|
||
if rc == 0:
|
||
self._msg("Готово", f"Профиль '{prof}' применён.")
|
||
else:
|
||
self._msg("Ошибка", f"Не удалось применить профиль '{prof}'. Код: {rc}", QtWidgets.QMessageBox.Critical)
|
||
|
||
def on_export(self):
|
||
prof = self.current_profile()
|
||
path, _ = QtWidgets.QFileDialog.getSaveFileName(
|
||
self,
|
||
"Экспорт профиля",
|
||
str(Path.home() / f"{prof}.json"),
|
||
"JSON (*.json);;All files (*)",
|
||
)
|
||
if not path:
|
||
return
|
||
|
||
try:
|
||
# Export does not require root
|
||
rc = _run(["/usr/sbin/saikyo-security-profile", "export", prof, path], check=False).returncode
|
||
if rc == 0:
|
||
self._msg("Готово", f"Профиль '{prof}' экспортирован в:\n{path}")
|
||
else:
|
||
self._msg("Ошибка", f"Не удалось экспортировать профиль '{prof}'. Код: {rc}", QtWidgets.QMessageBox.Critical)
|
||
except Exception as e:
|
||
self._msg("Ошибка", str(e), QtWidgets.QMessageBox.Critical)
|
||
|
||
def on_import(self):
|
||
prof = self.current_profile()
|
||
path, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||
self,
|
||
"Импорт профиля",
|
||
str(Path.home()),
|
||
"JSON (*.json);;All files (*)",
|
||
)
|
||
if not path:
|
||
return
|
||
|
||
# Validate JSON before asking for auth
|
||
try:
|
||
json.loads(Path(path).read_text(encoding="utf-8"))
|
||
except Exception as e:
|
||
self._msg("Ошибка", f"Некорректный JSON: {e}", QtWidgets.QMessageBox.Critical)
|
||
return
|
||
|
||
rc = _pkexec(["/usr/sbin/saikyo-security-profile", "import", prof, path]).returncode
|
||
if rc == 0:
|
||
self._msg("Готово", f"Профиль '{prof}' импортирован из:\n{path}")
|
||
self.refresh_preview()
|
||
else:
|
||
self._msg("Ошибка", f"Не удалось импортировать профиль '{prof}'. Код: {rc}", QtWidgets.QMessageBox.Critical)
|
||
|
||
|
||
def main(argv):
|
||
if not Path("/usr/sbin/saikyo-security-profile").exists():
|
||
print("ERROR: /usr/sbin/saikyo-security-profile not found", file=sys.stderr)
|
||
return 2
|
||
|
||
if not Path("/usr/bin/pkexec").exists():
|
||
print("ERROR: pkexec not found (install policykit-1)", file=sys.stderr)
|
||
return 2
|
||
|
||
app = QtWidgets.QApplication(list(argv))
|
||
app.setApplicationName("saikyo-security-profile-gui")
|
||
w = MainWindow()
|
||
w.show()
|
||
return app.exec_()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main(sys.argv))
|