Files
OSGKeyboard/Scripts/check_sensitive_logs.py
T
Rocky 9f308fadd2 feat(keyboard): ship AI hint carousel, home library cards, and clipboard polish
Rotate AI idle suggestions with optional remote packs, move history/dictionary onto self-sizing Home preview cards, harden clipboard capture/prompting, and simplify keyboard chrome by dropping most liquid-glass shadows.
2026-08-13 01:00:51 +08:00

246 lines
7.3 KiB
Python

#!/usr/bin/env python3
"""Reject obvious credential and user-text interpolation in Swift logs."""
from __future__ import annotations
import argparse
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
EXCLUDED_PARTS = {
".build",
".git",
"Carthage",
"DerivedData",
"OSGKeyboardExtTests",
"OSGKeyboardMacTests",
"OSGKeyboardTests",
"Pods",
"Scripts",
"Tests",
"ThirdParty",
"Vendor",
"build",
}
LOG_START = re.compile(
r"""
(?:
\bprint\s*\(
|\bOSGLog(?:\.\w+)+\s*\(
|\bOSGDiag\.log\s*\(
|\bFlowDiagnostics\.log\s*\(
|\bFlowTrace\.(?:capture|pipeline|asr|polish|keyboard|warn)\s*\(
|\bdebug\s*\(
|\b(?:log|logger)\.(?:debug|info|notice|warning|error|fault)\s*\(
)
""",
re.VERBOSE,
)
SENSITIVE_IDENTIFIER = re.compile(
r"""
\b(?:
apiKey
|asrApiKey
|Authorization
|httpBody
|bodyText
|responseBody
|result\.text
|error\.message
|(?:raw|final|full|partial)?Transcript
|(?:system|user|raw)?Prompt
|clipboard(?:Text|Content)?
)\b
""",
re.IGNORECASE | re.VERBOSE,
)
INTERPOLATION = re.compile(r"\\\((.*?)\)", re.DOTALL)
SENSITIVE_LABEL = re.compile(
r"\b(?:apiKey|Authorization|httpBody|bodyText|responseBody|transcript|prompt|clipboard)\s*[:=]",
re.IGNORECASE,
)
@dataclass(frozen=True)
class Violation:
path: Path
line: int
message: str
def swift_files(root: Path) -> list[Path]:
files: list[Path] = []
for current, directories, names in os.walk(root):
directories[:] = [
directory
for directory in directories
if directory not in EXCLUDED_PARTS
and not directory.endswith("Tests")
and not directory.startswith(".")
]
current_path = Path(current)
files.extend(current_path / name for name in names if name.endswith(".swift"))
return sorted(files)
def line_number(source: str, offset: int) -> int:
return source.count("\n", 0, offset) + 1
def extract_call(source: str, start: int) -> str:
depth = 0
saw_open = False
for index in range(start, min(len(source), start + 16_384)):
character = source[index]
if character == "(":
depth += 1
saw_open = True
elif character == ")" and saw_open:
depth -= 1
if depth == 0:
return source[start : index + 1]
return source[start : min(len(source), start + 16_384)]
def extract_transcript_function(source: str) -> tuple[int, str] | None:
match = re.search(r"\bfunc\s+transcript\s*\(", source)
if match is None:
return None
brace = source.find("{", match.end())
if brace < 0:
return None
depth = 0
for index in range(brace, len(source)):
if source[index] == "{":
depth += 1
elif source[index] == "}":
depth -= 1
if depth == 0:
return match.start(), source[brace : index + 1]
return match.start(), source[brace:]
def scan_file(path: Path) -> list[Violation]:
source = path.read_text(encoding="utf-8")
violations: list[Violation] = []
for match in LOG_START.finditer(source):
line_start = source.rfind("\n", 0, match.start()) + 1
if source[line_start : match.start()].lstrip().startswith("//"):
continue
call = extract_call(source, match.start())
expressions = INTERPOLATION.findall(call)
exposed_expressions = [
re.sub(r"\b(?:result\.text\?|error\.message)\.count\b", "", expression)
for expression in expressions
]
if any(SENSITIVE_IDENTIFIER.search(expression) for expression in exposed_expressions):
violations.append(
Violation(
path,
line_number(source, match.start()),
"sensitive identifier interpolated into a log",
)
)
continue
if SENSITIVE_LABEL.search(call) and (expressions or "+" in call):
violations.append(
Violation(
path,
line_number(source, match.start()),
"sensitive log label may expose user or credential text",
)
)
continue
opening = call.find("(")
direct_argument = call[opening + 1 :] if opening >= 0 else call
if SENSITIVE_IDENTIFIER.match(direct_argument.lstrip()):
violations.append(
Violation(
path,
line_number(source, match.start()),
"sensitive value passed directly to a log",
)
)
continue
if "CloudASR" in path.parts and re.search(
r"\\\([^)]*\.localizedDescription\b", call
):
violations.append(
Violation(
path,
line_number(source, match.start()),
"cloud ASR log exposes localized error detail",
)
)
if path.name == "FlowTrace.swift" or "enum FlowTrace" in source:
transcript_function = extract_transcript_function(source)
if transcript_function is not None:
offset, body = transcript_function
if re.search(r"\\\(\s*text\b", body):
violations.append(
Violation(
path,
line_number(source, offset),
"FlowTrace.transcript must not interpolate text",
)
)
return violations
def scan(paths: list[Path]) -> list[Violation]:
violations: list[Violation] = []
for path in paths:
violations.extend(scan_file(path))
return violations
def run_self_test() -> bool:
fixture_root = ROOT / "Scripts"
safe = fixture_root / "sensitive_logs_safe.fixture.swift"
unsafe = fixture_root / "sensitive_logs_unsafe.fixture.swift"
safe_violations = scan([safe])
unsafe_violations = scan([unsafe])
if safe_violations:
print("self-test failed: safe fixture was rejected", file=sys.stderr)
return False
if len(unsafe_violations) < 4:
print("self-test failed: unsafe fixture was not fully rejected", file=sys.stderr)
return False
print("Sensitive-log gate self-test passed.")
return True
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"--self-test",
action="store_true",
help="verify the scanner against safe and unsafe fixtures",
)
args = parser.parse_args()
if args.self_test:
return 0 if run_self_test() else 1
violations = scan(swift_files(ROOT))
if violations:
for violation in violations:
relative = violation.path.relative_to(ROOT)
print(f"{relative}:{violation.line}: {violation.message}", file=sys.stderr)
print(f"Sensitive-log gate failed with {len(violations)} violation(s).", file=sys.stderr)
return 1
print("Sensitive-log gate passed.")
return 0
if __name__ == "__main__":
raise SystemExit(main())