"""Daily access-drift scan — the evidence queries, run as controls. Kept apart from the pre-compile handler on purpose: one collects evidence on a schedule, the other looks for the conditions that evidence exists to find. Sharing a module would have made "which job opened this ticket" a question you answer by reading code. """ from __future__ import annotations from typing import Any from sqlalchemy.ext.asyncio import AsyncSession from app.scheduled_jobs.registry import register_scheduled_job from app.scheduled_jobs.slo import SLO_DAILY_DIGEST @register_scheduled_job( key="auditor.access_drift_scan", display_name="访问漂移扫描(默认只观察)", description=( "每天 04:10(Asia/Shanghai)用证据查询同一套确定性逻辑反过来做探测:" "离职者仍持有的访问、完成了却没有证据归档的离职、持有区间台账里同一 key " "的多个开区间、以及有角色却没有台账区间(时点查询看不见它)。\n" "**默认 observe_only=true:只算不开单。** 把一个新探测器放到多年累积的存量上," "第一天就可能刷出一个没人看的队列 —— 而没人看的队列比没有队列更糟。先用真实" "生产数据量一遍首日条数,确认不会成风暴,再置 " 'config={"observe_only": false} 开单。\n' "开单后走既有 findings 工单流(按 (control_key, dedup_key) 去重、消失即自动关单)," "推送只到 auditor / global_admin,不触达员工。\n" 'config={"grace_days":N} 覆写离职宽限期(默认 7 天)。' ), slo=SLO_DAILY_DIGEST, weight="heavy", ) async def run_access_drift_scan( db: AsyncSession, config: dict[str, Any], ) -> dict[str, Any]: """Run the access-drift detectors. Observe-only unless told otherwise.""" # Defaults to observe mode: the config key must be explicitly set to # ``false`` to start opening tickets. An absent key meaning "open # everything" is how a first deploy turns into a ticket storm. from app.domains.auditor.access_drift import LEAVER_GRACE_DAYS, run_access_drift # Value-based, not identity-based — but never silently. ``is not # False`` only ever recognised the JSON literal ``false``, so a # config saved as ``{"observe_only": 0}`` or ``{"observe_only": # "false"}`` — both of which a human editing job config would # reasonably write — stayed in observe mode forever with nothing # saying so. A switch that cannot be flipped is a switch nobody # trusts, and this one gates whether a control opens real tickets. # # Absent or null still means observe: that is the fail-safe default # and the reason a first deploy cannot produce a ticket storm. An # UNRECOGNISED value raises instead of falling back, because the two # possible silent readings are "you enabled something you didn't # mean to" and "you failed to enable what you meant to", and both # are worse than a loud failure. raw_observe = config.get("observe_only") if raw_observe is None: observe_only = True elif isinstance(raw_observe, bool): observe_only = raw_observe elif str(raw_observe).strip().lower() in ("false", "0", "no", "off"): observe_only = False elif str(raw_observe).strip().lower() in ("true", "1", "yes", "on"): observe_only = True else: raise RuntimeError( f"access drift: config.observe_only={raw_observe!r} is not a " "boolean; refusing to guess whether to open tickets" ) raw_grace = config.get("grace_days") grace_days = ( int(raw_grace) if isinstance(raw_grace, int | str) and str(raw_grace).isdigit() else LEAVER_GRACE_DAYS ) if raw_grace is not None and grace_days == LEAVER_GRACE_DAYS and ( not isinstance(raw_grace, int | str) or not str(raw_grace).isdigit() ): # A malformed value used to fall back to the default in silence, # so an operator who set ``"grace_days": "7 days"`` believed they # had widened the window and had not. raise RuntimeError( f"access drift: config.grace_days={raw_grace!r} is not a " "whole number of days" ) return await run_access_drift( db, observe_only=observe_only, grace_days=grace_days, )