mirror of
https://github.com/ringhyacinth/Star-Office-UI
synced 2026-04-21 13:27:19 +00:00
chore(security): add preflight checks and safe config templates
This commit is contained in:
parent
35067c5b8b
commit
0b322aa4e1
6 changed files with 251 additions and 11 deletions
18
.env.example
Normal file
18
.env.example
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Star Office UI - production environment example
|
||||
# Copy to .env (or your systemd/pm2 env file), then fill values.
|
||||
|
||||
# Mark production mode to enable startup hardening checks
|
||||
STAR_OFFICE_ENV=production
|
||||
|
||||
# Flask/session secret (REQUIRED in production)
|
||||
# Must be long/random (>=24 chars)
|
||||
FLASK_SECRET_KEY=replace_with_a_long_random_secret
|
||||
|
||||
# Asset drawer password (REQUIRED in production)
|
||||
# Do NOT use 1234 in production. Recommend >=8 chars.
|
||||
ASSET_DRAWER_PASS=replace_with_strong_drawer_password
|
||||
|
||||
# Optional Gemini runtime defaults
|
||||
# You can also set these in runtime-config.json via UI
|
||||
GEMINI_API_KEY=
|
||||
GEMINI_MODEL=nanobanana-pro
|
||||
66
README.md
66
README.md
|
|
@ -29,7 +29,11 @@ python3 -m pip install -r backend/requirements.txt
|
|||
# 3) 准备状态文件(首次)
|
||||
cp state.sample.json state.json
|
||||
|
||||
# 4) 启动后端
|
||||
# 4) (推荐)准备本地环境变量
|
||||
cp .env.example .env
|
||||
# 然后编辑 .env:至少设置 FLASK_SECRET_KEY 与 ASSET_DRAWER_PASS
|
||||
|
||||
# 5) 启动后端
|
||||
cd backend
|
||||
python3 app.py
|
||||
```
|
||||
|
|
@ -123,7 +127,14 @@ python3 -m pip install -r backend/requirements.txt
|
|||
cp state.sample.json state.json
|
||||
```
|
||||
|
||||
### 3) 启动后端
|
||||
### 3) (推荐)准备本地环境变量
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# 编辑 .env:至少设置 FLASK_SECRET_KEY 与 ASSET_DRAWER_PASS
|
||||
```
|
||||
|
||||
### 4) 启动后端
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
|
|
@ -143,6 +154,19 @@ python3 set_state.py idle "待命中"
|
|||
|
||||
---
|
||||
|
||||
## 3.1、安全自检(推荐上线前执行)
|
||||
|
||||
```bash
|
||||
python3 scripts/security_check.py
|
||||
```
|
||||
|
||||
- 返回 `Result: OK` 才建议进入公网部署。
|
||||
- 在生产模式(`STAR_OFFICE_ENV=production`)下,请务必配置强密码:
|
||||
- `FLASK_SECRET_KEY`(>=24 位随机字符串)
|
||||
- `ASSET_DRAWER_PASS`(不要使用 `1234`)
|
||||
|
||||
---
|
||||
|
||||
## 4、常用 API
|
||||
|
||||
- `GET /health`:健康检查
|
||||
|
|
@ -224,10 +248,13 @@ star-office-ui/
|
|||
...assets
|
||||
docs/
|
||||
screenshots/
|
||||
scripts/
|
||||
security_check.py
|
||||
office-agent-push.py
|
||||
set_state.py
|
||||
state.sample.json
|
||||
join-keys.json
|
||||
join-keys.sample.json
|
||||
.env.example
|
||||
SKILL.md
|
||||
README.md
|
||||
LICENSE
|
||||
|
|
@ -270,7 +297,11 @@ python3 -m pip install -r backend/requirements.txt
|
|||
# 3) Initialize state file (first run)
|
||||
cp state.sample.json state.json
|
||||
|
||||
# 4) Start backend
|
||||
# 4) (Recommended) prepare local env file
|
||||
cp .env.example .env
|
||||
# Then edit .env: set at least FLASK_SECRET_KEY and ASSET_DRAWER_PASS
|
||||
|
||||
# 5) Start backend
|
||||
cd backend
|
||||
python3 app.py
|
||||
```
|
||||
|
|
@ -342,7 +373,14 @@ python3 -m pip install -r backend/requirements.txt
|
|||
cp state.sample.json state.json
|
||||
```
|
||||
|
||||
### 3) Start backend
|
||||
### 3) (Recommended) prepare local env file
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Then edit .env: set at least FLASK_SECRET_KEY and ASSET_DRAWER_PASS
|
||||
```
|
||||
|
||||
### 4) Start backend
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
|
|
@ -362,6 +400,19 @@ python3 set_state.py idle "Standing by"
|
|||
|
||||
---
|
||||
|
||||
## III.1 Security preflight (recommended before public deployment)
|
||||
|
||||
```bash
|
||||
python3 scripts/security_check.py
|
||||
```
|
||||
|
||||
- Only deploy publicly when it returns `Result: OK`.
|
||||
- In production mode (`STAR_OFFICE_ENV=production`), set strong values for:
|
||||
- `FLASK_SECRET_KEY` (>=24 random chars)
|
||||
- `ASSET_DRAWER_PASS` (do not use `1234`)
|
||||
|
||||
---
|
||||
|
||||
## IV. Common APIs
|
||||
|
||||
- `GET /health`: Health check
|
||||
|
|
@ -450,10 +501,13 @@ star-office-ui/
|
|||
...assets
|
||||
docs/
|
||||
screenshots/
|
||||
scripts/
|
||||
security_check.py
|
||||
office-agent-push.py
|
||||
set_state.py
|
||||
state.sample.json
|
||||
join-keys.json
|
||||
join-keys.sample.json
|
||||
.env.example
|
||||
SKILL.md
|
||||
README.md
|
||||
LICENSE
|
||||
|
|
|
|||
|
|
@ -834,7 +834,15 @@ def state_to_area(state):
|
|||
if not os.path.exists(AGENTS_STATE_FILE):
|
||||
save_agents_state(DEFAULT_AGENTS)
|
||||
if not os.path.exists(JOIN_KEYS_FILE):
|
||||
save_join_keys({"keys": []})
|
||||
if os.path.exists(os.path.join(ROOT_DIR, "join-keys.sample.json")):
|
||||
try:
|
||||
with open(os.path.join(ROOT_DIR, "join-keys.sample.json"), "r", encoding="utf-8") as sf:
|
||||
sample = json.load(sf)
|
||||
save_join_keys(sample if isinstance(sample, dict) else {"keys": []})
|
||||
except Exception:
|
||||
save_join_keys({"keys": []})
|
||||
else:
|
||||
save_join_keys({"keys": []})
|
||||
|
||||
# Tighten runtime-config file perms if exists
|
||||
if os.path.exists(RUNTIME_CONFIG_FILE):
|
||||
|
|
@ -1983,6 +1991,20 @@ if __name__ == "__main__":
|
|||
print("=" * 50)
|
||||
print(f"State file: {STATE_FILE}")
|
||||
print("Listening on: http://0.0.0.0:18791")
|
||||
mode = "production" if _is_production_mode() else "development"
|
||||
print(f"Mode: {mode}")
|
||||
if _is_production_mode():
|
||||
print("Security hardening: ENABLED (strict checks)")
|
||||
else:
|
||||
weak_flags = []
|
||||
if not _is_strong_secret(str(app.secret_key)):
|
||||
weak_flags.append("weak FLASK_SECRET_KEY/STAR_OFFICE_SECRET")
|
||||
if not _is_strong_drawer_pass(ASSET_DRAWER_PASS_DEFAULT):
|
||||
weak_flags.append("weak ASSET_DRAWER_PASS")
|
||||
if weak_flags:
|
||||
print("Security hardening: WARNING (dev mode) -> " + ", ".join(weak_flags))
|
||||
else:
|
||||
print("Security hardening: OK")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=18791, debug=False)
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"keys": []
|
||||
}
|
||||
13
join-keys.sample.json
Normal file
13
join-keys.sample.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"keys": [
|
||||
{
|
||||
"key": "ocj_example_team_01",
|
||||
"used": false,
|
||||
"reusable": true,
|
||||
"maxConcurrent": 3,
|
||||
"usedBy": null,
|
||||
"usedByAgentId": null,
|
||||
"usedAt": null
|
||||
}
|
||||
]
|
||||
}
|
||||
136
scripts/security_check.py
Executable file
136
scripts/security_check.py
Executable file
|
|
@ -0,0 +1,136 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Star Office UI security preflight checker (non-destructive).
|
||||
|
||||
Checks:
|
||||
- weak/default secrets in env
|
||||
- risky tracked files in git index
|
||||
- known API key patterns in tracked files
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def run(cmd: list[str]) -> tuple[int, str, str]:
|
||||
p = subprocess.run(cmd, cwd=ROOT, capture_output=True, text=True)
|
||||
return p.returncode, p.stdout.strip(), p.stderr.strip()
|
||||
|
||||
|
||||
def is_strong_secret(v: str) -> bool:
|
||||
if not v:
|
||||
return False
|
||||
s = v.strip()
|
||||
if len(s) < 24:
|
||||
return False
|
||||
low = s.lower()
|
||||
for token in ("change-me", "default", "example", "test", "dev"):
|
||||
if token in low:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def is_strong_pass(v: str) -> bool:
|
||||
if not v:
|
||||
return False
|
||||
s = v.strip()
|
||||
if s == "1234":
|
||||
return False
|
||||
return len(s) >= 8
|
||||
|
||||
|
||||
def tracked_files() -> list[str]:
|
||||
code, out, _ = run(["git", "ls-files"])
|
||||
if code != 0:
|
||||
return []
|
||||
return [x for x in out.splitlines() if x.strip()]
|
||||
|
||||
|
||||
def file_has_secret_pattern(path: Path) -> list[str]:
|
||||
hits: list[str] = []
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8", errors="ignore")
|
||||
except Exception:
|
||||
return hits
|
||||
|
||||
patterns = [
|
||||
(r"AIza[0-9A-Za-z\-_]{20,}", "Google/Gemini API key-like token"),
|
||||
(r"sk-[A-Za-z0-9]{16,}", "Generic sk-* token"),
|
||||
(r"AKIA[0-9A-Z]{16}", "AWS access key-like token"),
|
||||
]
|
||||
for pat, label in patterns:
|
||||
if re.search(pat, text):
|
||||
hits.append(label)
|
||||
return hits
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("[security-check] Star Office UI preflight")
|
||||
|
||||
failures: list[str] = []
|
||||
warnings: list[str] = []
|
||||
|
||||
env_mode = (os.getenv("STAR_OFFICE_ENV") or os.getenv("FLASK_ENV") or "").strip().lower()
|
||||
in_prod = env_mode in {"prod", "production"}
|
||||
|
||||
secret = os.getenv("FLASK_SECRET_KEY") or os.getenv("STAR_OFFICE_SECRET") or ""
|
||||
drawer_pass = os.getenv("ASSET_DRAWER_PASS") or ""
|
||||
|
||||
if in_prod:
|
||||
if not is_strong_secret(secret):
|
||||
failures.append("Weak/missing FLASK_SECRET_KEY (or STAR_OFFICE_SECRET) in production")
|
||||
if not is_strong_pass(drawer_pass):
|
||||
failures.append("Weak/missing ASSET_DRAWER_PASS in production")
|
||||
else:
|
||||
if not secret:
|
||||
warnings.append("FLASK_SECRET_KEY not set (ok for local dev, not for production)")
|
||||
if not drawer_pass:
|
||||
warnings.append("ASSET_DRAWER_PASS not set (defaults may be unsafe for public exposure)")
|
||||
|
||||
tracked = tracked_files()
|
||||
risky_tracked = [
|
||||
"runtime-config.json",
|
||||
"join-keys.json",
|
||||
"office-agent-state.json",
|
||||
]
|
||||
for f in risky_tracked:
|
||||
if f in tracked:
|
||||
failures.append(f"Risky runtime file is tracked by git: {f}")
|
||||
|
||||
# scan tracked text-ish files for common secret patterns
|
||||
for rel in tracked:
|
||||
if rel.startswith(".git/"):
|
||||
continue
|
||||
p = ROOT / rel
|
||||
if not p.exists() or p.is_dir():
|
||||
continue
|
||||
if p.stat().st_size > 2_000_000:
|
||||
continue
|
||||
hits = file_has_secret_pattern(p)
|
||||
for h in hits:
|
||||
failures.append(f"Potential secret pattern in tracked file: {rel} ({h})")
|
||||
|
||||
if warnings:
|
||||
print("\nWarnings:")
|
||||
for w in warnings:
|
||||
print(f" - {w}")
|
||||
|
||||
if failures:
|
||||
print("\nFAIL:")
|
||||
for f in failures:
|
||||
print(f" - {f}")
|
||||
print("\nResult: FAILED")
|
||||
return 1
|
||||
|
||||
print("\nResult: OK")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Reference in a new issue