9f308fadd2
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.
246 lines
7.3 KiB
Python
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())
|