## Summary epic r04 - begin refactor ## Change Type - [x] Cowork feature - [ ] Bug fix - [ ] Core AI contribution - [ ] Test / hardening - [ ] Performance - [ ] Documentation ## Related Work Cowork Task: Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets Core AI Issue: Core Task: Related PR: ## Scope What is intentionally included? What is intentionally NOT included? ## Validation - [ ] Unit tests - [ ] Integration tests - [ ] Manual verification - [ ] Regression check Commands / evidence: ## Security Impact Permission / credential / network / customer data impact: ## Compatibility - [ ] No breaking change - [ ] Breaking change documented ## Reviewer Notes Anything Cowork reviewers should pay attention to. --------- Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com> Co-authored-by: Huong Le Thi Thien <huongltt35@fpt.com> Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com> Co-authored-by: Vu Dam Tuan <vudt15@fpt.com> Co-authored-by: Hiep Ha Van <hiephv3@fpt.com> Co-authored-by: Lam Hoang Van <lamhv7@fpt.com> Reviewed-on: #7 Co-authored-by: Duy Le Huu <duylh19@fpt.com>
This commit was merged in pull request #7.
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
"""CASAN Check 1 — không được có credential nào nằm phơi trong repo.
|
||||
|
||||
Team Gamma chủ trì check này (hạn: 30/08). Viết sẵn từ 21/08 để chạy được liên
|
||||
tục trong lúc chuyển API key sang Keyring (R02-T05), thay vì tới ngày cổng mới
|
||||
chạy lần đầu rồi mới biết còn sót.
|
||||
|
||||
Quét gì:
|
||||
* file cấu hình đã commit: ``*.json`` ``*.jsonl`` ``*.yaml`` ``*.yml`` ``*.env``
|
||||
* mã nguồn Python — chỗ gán chuỗi cho biến tên như api_key / token / secret
|
||||
|
||||
Tìm hai loại:
|
||||
1. Chuỗi có hình dạng credential thật (sk-…, ghp_…, xoxb-…, AKIA…, JWT…)
|
||||
2. Trường tên nhạy cảm mà giá trị không rỗng và không phải placeholder
|
||||
|
||||
Bỏ qua: chuỗi rỗng, placeholder ("your-key-here", "changeme"…), giá trị hằng
|
||||
không phải bí mật (Ollama đòi có api_key nhưng bỏ qua nội dung).
|
||||
|
||||
Chạy: python scripts/audit_security.py [--json]
|
||||
Mã thoát: 0 = sạch, 1 = có phát hiện.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
# console Windows hay là cp932/cp1258; ép UTF-8 để không chết giữa báo cáo
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
|
||||
SKIP_DIRS = {".git", "__pycache__", "node_modules", ".venv", "venv", "build",
|
||||
"dist", ".pytest_cache", ".mypy_cache", "cowork-local-gitea"}
|
||||
CONFIG_SUFFIX = {".json", ".jsonl", ".yaml", ".yml", ".env"}
|
||||
|
||||
# tên trường coi là nhạy cảm
|
||||
SENSITIVE = re.compile(
|
||||
r"(api[_-]?key|secret|token|password|passwd|client[_-]?secret|"
|
||||
r"access[_-]?key|private[_-]?key|credential)", re.I)
|
||||
|
||||
# hình dạng credential thật — bắt được kể cả khi tên trường vô hại
|
||||
SHAPES = [
|
||||
("OpenAI", re.compile(r"\bsk-[A-Za-z0-9_\-]{20,}")),
|
||||
("Anthropic", re.compile(r"\bsk-ant-[A-Za-z0-9_\-]{20,}")),
|
||||
("GitHub", re.compile(r"\bgh[pousr]_[A-Za-z0-9]{30,}")),
|
||||
("Slack", re.compile(r"\bxox[abprs]-[A-Za-z0-9\-]{10,}")),
|
||||
("AWS", re.compile(r"\bAKIA[0-9A-Z]{16}\b")),
|
||||
("Google", re.compile(r"\bAIza[0-9A-Za-z_\-]{35}\b")),
|
||||
("JWT", re.compile(r"\beyJ[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\.")),
|
||||
("Private key", re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----")),
|
||||
]
|
||||
|
||||
#: Dòng có dấu này được bỏ qua — lối thoát chuẩn cho mẫu thử, tài liệu, hằng
|
||||
#: đặt tên chứa "secret". Bắt buộc ghi lý do sau dấu hai chấm.
|
||||
ALLOW_MARK = re.compile(r"#\s*casan:\s*allow")
|
||||
|
||||
#: Giá trị là KHOÁ i18n / tên hằng, không phải bí mật. Bắt bằng hình dạng
|
||||
#: "a.b.c" hoặc "a_b_c" chứ không phải bằng danh sách đen từng chữ.
|
||||
LOOKS_LIKE_KEY = re.compile(r"^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$")
|
||||
|
||||
#: Credential thật gần như luôn dài hơn thế này. Ngưỡng để loại dữ liệu test
|
||||
#: kiểu api_key="x" — báo động giả làm cả đội thôi đọc báo cáo.
|
||||
MIN_SECRET_LEN = 12
|
||||
|
||||
#: Giá trị là hằng liệt kê, không phải bí mật: mức độ cảnh báo, bật/tắt…
|
||||
ENUMISH = {"warning", "warn", "error", "info", "debug", "critical", "on", "off",
|
||||
"true", "false", "yes", "no", "allow", "deny", "block", "ask",
|
||||
"always", "never", "auto", "default", "disabled", "enabled"}
|
||||
|
||||
# giá trị vô hại — không tính là phát hiện
|
||||
PLACEHOLDER = re.compile(
|
||||
r"^(|ollama|none|null|changeme|your[_\- ]?(api[_\- ]?)?key([_\- ]?here)?|"
|
||||
r"<[^>]*>|\{\{.*\}\}|\$\{.*\}|xxx+|\*+|placeholder|todo|example|test|dummy|"
|
||||
r"sk-\.\.\.|\.\.\.)$", re.I)
|
||||
|
||||
# gán chuỗi trong Python: api_key = "..."
|
||||
PY_ASSIGN = re.compile(
|
||||
r"""["']?(\w*(?:api[_-]?key|secret|token|password|credential)\w*)["']?\s*[:=]\s*"""
|
||||
r"""["']([^"']*)["']""", re.I)
|
||||
|
||||
|
||||
def _is_placeholder(value: str) -> bool:
|
||||
v = value.strip()
|
||||
if PLACEHOLDER.match(v) or v.lower() in ENUMISH:
|
||||
return True
|
||||
if LOOKS_LIKE_KEY.match(v): # "monitoring.action_secret_in_output"
|
||||
return True
|
||||
# quá ngắn để là credential thật
|
||||
return len(v) < MIN_SECRET_LEN
|
||||
|
||||
|
||||
def _walk():
|
||||
for path in REPO.rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
if any(part in SKIP_DIRS for part in path.parts):
|
||||
continue
|
||||
if path.suffix in CONFIG_SUFFIX or path.suffix == ".py":
|
||||
yield path
|
||||
|
||||
|
||||
def scan() -> list[dict]:
|
||||
findings: list[dict] = []
|
||||
for path in _walk():
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
continue
|
||||
rel = path.relative_to(REPO).as_posix()
|
||||
|
||||
for lineno, line in enumerate(text.splitlines(), 1):
|
||||
if ALLOW_MARK.search(line):
|
||||
continue
|
||||
# 1. hình dạng credential thật
|
||||
for label, pattern in SHAPES:
|
||||
m = pattern.search(line)
|
||||
if m:
|
||||
findings.append({
|
||||
"file": rel, "line": lineno, "kind": f"{label} credential",
|
||||
"evidence": m.group(0)[:12] + "…",
|
||||
})
|
||||
|
||||
# 2. trường nhạy cảm có giá trị
|
||||
for m in PY_ASSIGN.finditer(line):
|
||||
field, value = m.group(1), m.group(2)
|
||||
if not SENSITIVE.search(field) or _is_placeholder(value):
|
||||
continue
|
||||
findings.append({
|
||||
"file": rel, "line": lineno,
|
||||
"kind": f"trường '{field}' có giá trị",
|
||||
"evidence": value[:6] + "…" if len(value) > 6 else value,
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def _self_test() -> int:
|
||||
"""Một máy quét không tìm thấy gì chỉ có giá trị nếu chứng minh được nó
|
||||
biết tìm. Cắm mẫu xấu và mẫu vô hại, xem có phân biệt đúng không."""
|
||||
import tempfile
|
||||
|
||||
bad = {
|
||||
"OpenAI": '"api_key": "sk-proj-abc123def456ghi789jkl012mno"', # casan: allow - mau thu cua chinh script
|
||||
"GitHub": 'token = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"', # casan: allow - mau thu cua chinh script
|
||||
"AWS": 'aws = "AKIAIOSFODNN7EXAMPLE"', # casan: allow - mau thu cua chinh script
|
||||
"Anthropic": '"api_key": "sk-ant-api03-xxxxxxxxxxxxxxxxxxxxxx"', # casan: allow - mau thu cua chinh script
|
||||
}
|
||||
ok = {
|
||||
"rỗng": '"api_key": ""',
|
||||
"placeholder": '"api_key": "your-key-here"',
|
||||
"ollama": '"api_key": "ollama"',
|
||||
"test ngắn": 'api_key = "x"',
|
||||
"hằng liệt kê": '"secret_in_output": "warning"',
|
||||
}
|
||||
global REPO
|
||||
keep = REPO
|
||||
passed = True
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
REPO = Path(tmp)
|
||||
for label, line in {**bad, **ok}.items():
|
||||
(REPO / "probe.py").write_text(line + "\n", encoding="utf-8")
|
||||
found = bool(scan())
|
||||
want = label in bad
|
||||
mark = "OK " if found == want else "SAI"
|
||||
if found != want:
|
||||
passed = False
|
||||
verb = "bắt được" if found else "bỏ qua"
|
||||
print(f" [{mark}] {label:14} -> {verb}")
|
||||
REPO = keep
|
||||
print()
|
||||
print("Tự kiểm: " + ("script phân biệt đúng." if passed
|
||||
else "*** script phân biệt SAI ***"))
|
||||
return 0 if passed else 1
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="CASAN Check 1 — quét credential lộ")
|
||||
ap.add_argument("--json", action="store_true", help="in kết quả dạng JSON")
|
||||
ap.add_argument("--self-test", action="store_true",
|
||||
help="cắm credential giả vào file tạm, kiểm script có bắt được")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.self_test:
|
||||
return _self_test()
|
||||
|
||||
findings = scan()
|
||||
if args.json:
|
||||
print(json.dumps(findings, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
n_files = sum(1 for _ in _walk())
|
||||
print(f"CASAN Check 1 — quét {n_files} file trong {REPO.name}/")
|
||||
if not findings:
|
||||
print("\n0 credential lưu plaintext. PASS.")
|
||||
else:
|
||||
print(f"\n*** {len(findings)} phát hiện ***\n")
|
||||
for f in findings:
|
||||
print(f" {f['file']}:{f['line']}")
|
||||
print(f" {f['kind']} — {f['evidence']}")
|
||||
return 1 if findings else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user