选择国家与语言当前国家: 中国
德勤 Virtual Internship / 任务 03
实习猿原创实训 · 任务 03导出接口访问调查
以策略、会话和请求证据识别控制缺口,给出可检验的处置建议。
先查看已完成的成果,再使用材料完成自己的交付。
先阅读完整成果材料
learner/materials/access-log.csv 阅读完整文件
下载文件| request_id | at | user | session | role | method | path | status | rows | bytes | ip |
|---|---|---|---|---|---|---|---|---|---|---|
| Q0001 | 2026-09-10T08:00:00Z | op-1 | s-0 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0002 | 2026-09-10T08:01:00Z | op-1 | s-0 | operator | GET | /orders?hub=海港 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0003 | 2026-09-10T08:02:00Z | op-2 | s-1 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0004 | 2026-09-10T08:03:00Z | op-2 | s-1 | operator | GET | /orders?hub=东城 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0005 | 2026-09-10T08:04:00Z | op-3 | s-2 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0006 | 2026-09-10T08:05:00Z | op-3 | s-2 | operator | GET | /orders?hub=西岭 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0007 | 2026-09-10T08:06:00Z | op-4 | s-3 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0008 | 2026-09-10T08:07:00Z | op-4 | s-3 | operator | GET | /orders?hub=南湾 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0009 | 2026-09-10T08:08:00Z | op-1 | s-4 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0010 | 2026-09-10T08:09:00Z | op-1 | s-4 | operator | GET | /orders?hub=海港 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0011 | 2026-09-10T08:10:00Z | op-2 | s-5 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0012 | 2026-09-10T08:11:00Z | op-2 | s-5 | operator | GET | /orders?hub=东城 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0013 | 2026-09-10T08:12:00Z | op-3 | s-6 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0014 | 2026-09-10T08:13:00Z | op-3 | s-6 | operator | GET | /orders?hub=西岭 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0015 | 2026-09-10T08:14:00Z | op-4 | s-7 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0016 | 2026-09-10T08:15:00Z | op-4 | s-7 | operator | GET | /orders?hub=南湾 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0017 | 2026-09-10T08:16:00Z | op-1 | s-8 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0018 | 2026-09-10T08:17:00Z | op-1 | s-8 | operator | GET | /orders?hub=海港 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0019 | 2026-09-10T08:18:00Z | op-2 | s-9 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0020 | 2026-09-10T08:19:00Z | op-2 | s-9 | operator | GET | /orders?hub=东城 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0021 | 2026-09-10T08:20:00Z | op-3 | s-10 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0022 | 2026-09-10T08:21:00Z | op-3 | s-10 | operator | GET | /orders?hub=西岭 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0023 | 2026-09-10T08:22:00Z | op-4 | s-11 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0024 | 2026-09-10T08:23:00Z | op-4 | s-11 | operator | GET | /orders?hub=南湾 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0025 | 2026-09-10T08:24:00Z | op-1 | s-12 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0026 | 2026-09-10T08:25:00Z | op-1 | s-12 | operator | GET | /orders?hub=海港 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0027 | 2026-09-10T08:26:00Z | op-2 | s-13 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0028 | 2026-09-10T08:27:00Z | op-2 | s-13 | operator | GET | /orders?hub=东城 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0029 | 2026-09-10T08:28:00Z | op-3 | s-14 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0030 | 2026-09-10T08:29:00Z | op-3 | s-14 | operator | GET | /orders?hub=西岭 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0031 | 2026-09-10T08:30:00Z | op-4 | s-15 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0032 | 2026-09-10T08:31:00Z | op-4 | s-15 | operator | GET | /orders?hub=南湾 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0033 | 2026-09-10T08:32:00Z | op-1 | s-16 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0034 | 2026-09-10T08:33:00Z | op-1 | s-16 | operator | GET | /orders?hub=海港 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0035 | 2026-09-10T08:34:00Z | op-2 | s-17 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0036 | 2026-09-10T08:35:00Z | op-2 | s-17 | operator | GET | /orders?hub=东城 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0037 | 2026-09-10T08:36:00Z | op-3 | s-18 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0038 | 2026-09-10T08:37:00Z | op-3 | s-18 | operator | GET | /orders?hub=西岭 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0039 | 2026-09-10T08:38:00Z | op-4 | s-19 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0040 | 2026-09-10T08:39:00Z | op-4 | s-19 | operator | GET | /orders?hub=南湾 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0041 | 2026-09-10T08:40:00Z | op-1 | s-20 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0042 | 2026-09-10T08:41:00Z | op-1 | s-20 | operator | GET | /orders?hub=海港 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0043 | 2026-09-10T08:42:00Z | op-2 | s-21 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0044 | 2026-09-10T08:43:00Z | op-2 | s-21 | operator | GET | /orders?hub=东城 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0045 | 2026-09-10T08:44:00Z | op-3 | s-22 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0046 | 2026-09-10T08:45:00Z | op-3 | s-22 | operator | GET | /orders?hub=西岭 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0047 | 2026-09-10T08:46:00Z | op-4 | s-23 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0048 | 2026-09-10T08:47:00Z | op-4 | s-23 | operator | GET | /orders?hub=南湾 | 200 | 20 | 4000 | 192.0.2.10 |
| Q0049 | 2026-09-10T09:20:00Z | svc-archive | svc-1 | service | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0050 | 2026-09-10T09:21:00Z | svc-archive | svc-1 | service | GET | /export?hub=ALL | 200 | 10000 | 800000 | 192.0.2.10 |
| Q0051 | 2026-09-10T09:22:00Z | svc-archive | svc-1 | service | GET | /export?hub=ALL | 200 | 10000 | 800000 | 192.0.2.10 |
| Q0052 | 2026-09-10T09:23:00Z | svc-archive | svc-1 | service | GET | /export?hub=ALL | 200 | 10000 | 800000 | 192.0.2.10 |
| Q0053 | 2026-09-10T09:24:00Z | svc-archive | svc-1 | service | GET | /export?hub=ALL | 200 | 10000 | 800000 | 192.0.2.10 |
| Q0054 | 2026-09-10T09:25:00Z | svc-archive | svc-1 | service | GET | /export?hub=ALL | 200 | 10000 | 800000 | 192.0.2.10 |
| Q0055 | 2026-09-10T09:30:00Z | lead-1 | lead-1 | supervisor | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0056 | 2026-09-10T09:31:00Z | lead-1 | lead-1 | supervisor | GET | /export?hub=海港 | 200 | 1000 | 80000 | 192.0.2.10 |
| Q0057 | 2026-09-10T09:40:00Z | op-7 | sus-7 | operator | POST | /login | 200 | 0 | 0 | 198.51.100.7 |
| Q0058 | 2026-09-10T09:41:00Z | op-7 | sus-7 | operator | GET | /export?hub=ALL | 200 | 1200 | 96000 | 198.51.100.7 |
| Q0059 | 2026-09-10T09:42:00Z | op-7 | sus-7 | operator | GET | /export?hub=ALL | 200 | 1200 | 96000 | 198.51.100.7 |
| Q0060 | 2026-09-10T09:43:00Z | op-7 | sus-7 | operator | GET | /export?hub=ALL | 200 | 1200 | 96000 | 198.51.100.7 |
| Q0061 | 2026-09-10T09:44:00Z | op-7 | sus-7 | operator | GET | /export?hub=ALL | 200 | 1200 | 96000 | 198.51.100.7 |
| Q0062 | 2026-09-10T09:45:00Z | op-7 | sus-7 | operator | GET | /export?hub=ALL | 200 | 1200 | 96000 | 198.51.100.7 |
| Q0063 | 2026-09-10T09:46:00Z | op-7 | sus-7 | operator | GET | /export?hub=ALL | 200 | 1200 | 96000 | 198.51.100.7 |
| Q0064 | 2026-09-10T10:00:00Z | unknown | none | anonymous | GET | /export?hub=ALL | 401 | 0 | 0 | 203.0.113.9 |
| Q0065 | 2026-09-10T10:01:00Z | unknown | none | anonymous | GET | /export?hub=ALL | 401 | 0 | 0 | 203.0.113.9 |
| Q0066 | 2026-09-10T10:02:00Z | unknown | none | anonymous | GET | /export?hub=ALL | 401 | 0 | 0 | 203.0.113.9 |
| Q0067 | 2026-09-10T10:03:00Z | unknown | none | anonymous | GET | /export?hub=ALL | 401 | 0 | 0 | 203.0.113.9 |
| Q0068 | 2026-09-10T10:30:00Z | op-2 | expired-2 | operator | POST | /login | 200 | 0 | 0 | 192.0.2.10 |
| Q0069 | 2026-09-10T11:15:00Z | op-2 | expired-2 | operator | GET | /orders?hub=东城 | 200 | 50 | 10000 | 192.0.2.10 |
learner/materials/access-policy.json 阅读完整文件
下载文件{
"session_ttl_minutes": 30,
"export_roles": [
"supervisor",
"service"
],
"operator_hub": {
"op-1": "海港",
"op-2": "东城",
"op-3": "西岭",
"op-4": "南湾",
"op-7": "海港"
},
"network_note": "经企业 VPN 访问网关;IP 为文档保留地址,不能证明真实地理位置或攻击者身份。",
"service_accounts": [
"svc-archive"
]
}learner/materials/data-dictionary.md 阅读完整文件
下载文件数据字典与口径
业务工作单
你加入冷链履约运营项目组。运营负责人需要回答:哪些订单与站点需要先处理、导出接口哪里存在控制缺口,以及下一版工作台应交付什么。四个站点为海港、东城、西岭、南湾。standard 承诺 24 h,express 承诺 8 h;具体截止时间以 orders.csv 的 due_at 为准。
材料覆盖 2026 年 9 月的订单及事件,统一复盘截止点为 rules.json 的 cutoff。所有业务时间转 UTC;本地显示时可以换时区,但计算不能把无时区字符串当成本地时间。输入可包含截止点之后的事件,用于检查是否错误使用未来信息。
orders.csv
- order_id:唯一订单号,字符串,不转成数字。
- hub:站点名;service:standard/express;route:线路编号。
- created_at:带时区的下单时刻;due_at:承诺交付时刻,UTC ISO 8601。
- 一行代表一个订单,事件不等于订单;不要按事件行数统计订单。
两个事件源
- A:event_id → event_id;order_ref → order_id;hub → hub;service → service;event → kind;event_time → happened_at。
- B:id → event_id;shipment.number → order_id;shipment.station → hub;shipment.product → service;action 的 OPEN/COLLECT/DROP/VOID 映射 created/picked_up/delivered/cancelled;at_ms 为整数 Unix 毫秒,输出为 UTC ISO 时间。
- 标准事件必须且只需包含 event_id、order_id、hub、service、kind、happened_at。来源文件与行号写入异常/重复清单。
- 同 event_id 标准内容完全相同:只留一条,其他计 exact_replay。同 event_id 内容不同:整组隔离,不能先到先得。坏字段、未知动作或无时区时间:隔离并保留来源及原因。
- 禁止原地修改原始材料。对账:原始事件数 = 标准事件数 + 隔离记录数 + 重复剔除数。
截止日履约口径
按订单聚合,先只保留 happened_at ≤ cutoff 的事件。取消单优先排除;due_at > cutoff 的非取消单记 not_mature。其他订单:截止前无交付记 overdue_open;首次交付时刻 > due_at 记 delivered_late;其余 on_time。恰好到期算成熟,恰好交付算准时。
成熟分母 = on_time + delivered_late + overdue_open。迟交分子 = delivered_late + overdue_open。取消与未到期均不得填为准时。已交付迟交分钟按交付时间减 due_at,未交付逾期分钟按 cutoff 减 due_at,两者单列。此材料不包含退款、食物损坏或实际成本,不能推出这些金额。
服务审查口径
rules.json 固定目标迟交率为 10%,最小成熟样本 15。差距用百分点,不用百分比变化。成熟样本 <15 优先归“待补样本”;差距 >15 个百分点归“优先处理”;>5 且 ≤15 归“需要复核”;≤5 归“达到阈值”。这些是本次运营审查规则,不是统计显著性或公平性的证明。
统一服务结构比较:standard 和 express 各 50% 权重。两个服务层均满足最小样本才计算站点标准化迟交率;否则保留空值并说明缺口。不得用无依据的总体平均填补小样本。业务假设须与观察事实分开。
访问日志与授权策略
access-log.csv 一行一个请求。request_id 唯一;at 为 UTC;user、session、role 为请求上下文;method、path 为路由;status 为响应码;rows、bytes 是网关响应计数。它们不证明唯一客户数,也不证明接收者已使用数据。
access-policy.json 是本次调查的授权基线。operator 只能读取被授权站点,不能 export。supervisor 与 service 允许导出。登录成功后 session 有效 30 分钟,恰好 30 分钟有效;之后应拒绝。401 请求代表被拒绝的尝试,不是成功泄露。svc-archive 是批准的归档账号,频繁大批导出不能只因次数高就定性。日志没有文件正文、终端取证或 VPN 会话记录,不能据此归属自然人、地理位置或入口路径。
步骤
- 先把策略转成三条检查:导出角色权限、站点范围、session 有效期。列出判断所需字段,不用 IP 地理猜测身份。
- 按 user+session 匹配成功登录与后续请求,计算经过分钟。30 分钟边界有效,超时后 200 响应需调查。登录失败不能续期。
- 只对成功响应统计可能返回的 rows 与 bytes。对 401 单列尝试,不把它加到成功导出。
- 列出 request_id、时间、账号、策略差异和响应量,区分一个请求触犯多个规则与多个事件,避免重复计量。
- 检查 svc-archive 与 lead-1 的高量导出,说明为什么规则允许它们。给出一个仅按频次检测会误报的反证。
- 写 incident-report.md:时间线、影响范围、已知/未知、P0 处置建议与负责人、验证及回退。任何禁用账号、撤销 session 都是建议,实训不触碰实际账号。
完整成果
访问控制事件调查报告
发现与严重程度
优先级 P0:operator 账号 op-7 在有效 session sus-7 下成功导出 ALL 站点,违反导出角色与站点范围两条规则。09:41–09:46 UTC 共 6 条成功响应,证据 Q0058–Q0063。网关记录返回量累计 7,200 行、576,000 字节。该数量不是唯一客户数,日志不证明文件被保存、传播或被哪位自然人操作。
另一个独立缺口:op-2 的 expired-2 在 10:30 登录,11:15 仍收到 200,间隔 45 分钟,超过 30 分钟有效期,证据 Q0068 与 Q0069。它返回 50 行、10,000 字节,单独记录,不加到 op-7 导出量。
时间线与反证
- Q0049–Q0054:svc-archive 登录并执行 5 次 10,000 行归档。它在策略允许列表且角色 service,有效 session 下允许导出,不应按频次高直接定性。
- Q0055–Q0056:lead-1 为 supervisor,受控导出海港,策略允许。
- Q0057:op-7 登录;Q0058–Q0063:角色仍是 operator,却返回全部站点导出。
- Q0064–Q0067:匿名访问返回 401,仅证明尝试被拒绝,不证明成功读取。
- Q0068–Q0069:45 分钟后过期会话仍被接受。
处置建议
P0:安全值班人员先保全网关日志、导出审计与身份服务登录记录,记录哈希和访问权限。授权运维人员临时关闭 operator 导出路由,并对 sus-7 撤销会话;本次实训没有执行这些实际操作。产品负责人检查是否有其他受影响导出批次;合规负责人结合真实文件内容判断后续义务,当前日志不足以判断。
P1:API 在每次请求上检查角色和站点范围;对 session 强制执行 TTL;service 单独配置最小导出范围并记录批次标识。告警按成功越权而不是单纯频率设计。
验证与回退
修复验收:operator /export 返回 403,operator 读取他站点返回 403;有效 supervisor 与 svc-archive 返回 200;session 恰好 30 分钟允许、超过 30 分钟返回 401。回归 /orders 的合法访问。若误阻断业务,保留 operator 导出禁用,通过已授权 supervisor 完成必要受控导出,不取消访问检查。
未知:入口是否经 VPN、凭据是否被盗、自然人身份、文件是否离开终端。需要 VPN 会话与终端证据,不因 IP 字段就下结论。
下载文件参考答案
instructor/answers/security-findings.csv 阅读完整文件
下载文件| request_id | at | user | session | finding | status | rows | bytes |
|---|---|---|---|---|---|---|---|
| Q0058 | 2026-09-10T09:41:00Z | op-7 | sus-7 | unauthorised_export;hub_scope_violation | 200 | 1200 | 96000 |
| Q0059 | 2026-09-10T09:42:00Z | op-7 | sus-7 | unauthorised_export;hub_scope_violation | 200 | 1200 | 96000 |
| Q0060 | 2026-09-10T09:43:00Z | op-7 | sus-7 | unauthorised_export;hub_scope_violation | 200 | 1200 | 96000 |
| Q0061 | 2026-09-10T09:44:00Z | op-7 | sus-7 | unauthorised_export;hub_scope_violation | 200 | 1200 | 96000 |
| Q0062 | 2026-09-10T09:45:00Z | op-7 | sus-7 | unauthorised_export;hub_scope_violation | 200 | 1200 | 96000 |
| Q0063 | 2026-09-10T09:46:00Z | op-7 | sus-7 | unauthorised_export;hub_scope_violation | 200 | 1200 | 96000 |
| Q0069 | 2026-09-10T11:15:00Z | op-2 | expired-2 | expired_or_missing_session | 200 | 50 | 10000 |
完整参考实现 阅读完整文件
下载文件"""Reference implementation for the Ape Intern cold-chain operations lab."""
import csv,json
from collections import defaultdict,Counter
from datetime import datetime,timezone
from pathlib import Path
from urllib.parse import urlparse,parse_qs
UTC=timezone.utc
KINDS={'OPEN':'created','COLLECT':'picked_up','DROP':'delivered','VOID':'cancelled'}
HUBS={'海港','东城','西岭','南湾'}
def parse_time(value):
t=datetime.fromisoformat(value.replace('Z','+00:00'))
if t.tzinfo is None:raise ValueError('timezone_required')
return t.astimezone(UTC)
def iso(t):return t.isoformat(timespec='seconds').replace('+00:00','Z')
def readcsv(p):
with Path(p).open(encoding='utf-8-sig',newline='') as f:return list(csv.DictReader(f))
def writecsv(p,rows,fields=None):
rows=list(rows)
with Path(p).open('w',encoding='utf-8-sig',newline='') as f:
w=csv.DictWriter(f,fieldnames=fields or list(rows[0]));w.writeheader();w.writerows(rows)
def dump(p,value):Path(p).write_text(json.dumps(value,ensure_ascii=False,indent=2)+'\n')
def _normalize(record,source):
if source=='A':
e=dict(event_id=record['event_id'],order_id=record['order_ref'],hub=record['hub'],service=record['service'],kind=record['event'],happened_at=iso(parse_time(record['event_time'])))
else:
ms=record['at_ms']
if type(ms) is not int:raise ValueError('epoch_must_be_integer')
e=dict(event_id=record['id'],order_id=record['shipment']['number'],hub=record['shipment']['station'],service=record['shipment']['product'],kind=KINDS[record['action']],happened_at=iso(datetime.fromtimestamp(ms/1000,UTC)))
if e['kind'] not in KINDS.values():raise ValueError('unsupported_event')
if e['hub'] not in HUBS or e['service'] not in {'standard','express'}:raise ValueError('unknown_dimension')
if not e['event_id'] or not e['order_id']:raise ValueError('empty_key')
return e
def normalize(record,source):
try:return _normalize(record,source)
except (KeyError,TypeError,OverflowError) as error:raise ValueError(str(error).strip("'")) from error
def ingest(data):
grouped=defaultdict(list);rejected=[];duplicates=[]
for source,name in [('A','dispatch-a.jsonl'),('B','courier-b.jsonl')]:
for line,text in enumerate((data/name).read_text().splitlines(),1):
loc={'source':name,'line':line}
try:
event=normalize(json.loads(text),source);grouped[event['event_id']].append((event,loc))
except (ValueError,KeyError,TypeError,OverflowError) as ex:
rejected.append(dict(loc,reason=str(ex).strip("'")))
accepted=[]
for eid,items in grouped.items():
distinct={json.dumps(e,sort_keys=True,ensure_ascii=False) for e,loc in items}
if len(distinct)>1:
rejected.extend(dict(loc,reason='immutable_id_conflict') for e,loc in items);continue
accepted.append(items[0][0])
duplicates.extend(dict(loc,event_id=eid,reason='exact_replay') for e,loc in items[1:])
accepted.sort(key=lambda e:(e['happened_at'],e['event_id']))
return accepted,rejected,duplicates
def classify_order(order,events,cutoff):
# Only facts available at cutoff can influence a retrospective result.
visible=[e for e in events if parse_time(e['happened_at'])<=cutoff]
visible.sort(key=lambda e:parse_time(e['happened_at']))
delivered=[parse_time(e['happened_at']) for e in visible if e['kind']=='delivered']
cancelled=any(e['kind']=='cancelled' for e in visible)
due=parse_time(order['due_at'])
if cancelled:state='cancelled'
elif due>cutoff:state='not_mature'
elif not delivered:state='overdue_open'
elif min(delivered)>due:state='delivered_late'
else:state='on_time'
delay=round((min(delivered)-due).total_seconds()/60) if state=='delivered_late' else 0
overdue=round((cutoff-due).total_seconds()/60) if state=='overdue_open' else 0
return dict(order,cohort=state,late_minutes=delay,overdue_minutes=overdue)
def aggregate(rows,keys):
groups=defaultdict(list)
for row in rows:groups[tuple(row[k] for k in keys)].append(row)
result=[]
for dims,records in sorted(groups.items()):
counts=Counter(r['cohort'] for r in records)
eligible=sum(counts[x] for x in ['on_time','delivered_late','overdue_open'])
late=counts['delivered_late']+counts['overdue_open']
result.append(dict(zip(keys,dims),total=len(records),eligible=eligible,late=late,late_rate=round(late/eligible,6) if eligible else None,on_time=counts['on_time'],delivered_late=counts['delivered_late'],overdue_open=counts['overdue_open'],cancelled=counts['cancelled'],not_mature=counts['not_mature'],delivered_late_minutes=sum(r['late_minutes'] for r in records),open_overdue_minutes=sum(r['overdue_minutes'] for r in records)))
return result
def service_band(rate,n,rules):
if n<rules['min_sample']:return '待补样本'
gap=(rate-rules['late_target'])*100
if gap>rules['priority_gap_pp']:return '优先处理'
if gap>rules['review_gap_pp']:return '需要复核'
return '达到阈值'
def fairness(hub_tiers,rules):
rows=[]
for x in hub_tiers:
rate=x['late_rate'];rows.append(dict(hub=x['hub'],service=x['service'],eligible=x['eligible'],late=x['late'],late_rate=rate,target=rules['late_target'],gap_pp=round((rate-rules['late_target'])*100,3) if rate is not None else None,review=service_band(rate,x['eligible'],rules)))
standardized=[]
for hub in sorted(HUBS):
parts=[r for r in rows if r['hub']==hub]
enough=len(parts)==2 and all(r['eligible']>=rules['min_sample'] for r in parts)
value=sum(r['late_rate']*rules[r['service']+'_weight'] for r in parts) if enough else None
standardized.append(dict(hub=hub,standardized_late_rate=round(value,6) if value is not None else None,status='可比较' if enough else '待补样本'))
return rows,standardized
def investigate(logs,policy):
logins={};findings=[]
for row in sorted(logs,key=lambda r:(r['at'],r['request_id'])):
t=parse_time(row['at']);status=int(row['status']);route=urlparse(row['path']);query=parse_qs(route.query)
if route.path=='/login' and status==200:logins[(row['user'],row['session'])]=t;continue
if status!=200:continue
types=[]
if route.path=='/export' and row['role'] not in policy['export_roles']:types.append('unauthorised_export')
hub=query.get('hub',[''])[0]
if row['role']=='operator' and hub and hub!=policy['operator_hub'].get(row['user']):types.append('hub_scope_violation')
login=logins.get((row['user'],row['session']))
if login is None or (t-login).total_seconds()>policy['session_ttl_minutes']*60:types.append('expired_or_missing_session')
if types:findings.append(dict(request_id=row['request_id'],at=row['at'],user=row['user'],session=row['session'],finding=';'.join(types),status=status,rows=int(row['rows']),bytes=int(row['bytes'])))
return findings
def solve(data,out):
data=Path(data);out=Path(out);out.mkdir(parents=True,exist_ok=True)
events,rejects,dups=ingest(data)
(out/'canonical-events.jsonl').write_text(''.join(json.dumps(e,ensure_ascii=False)+'\n' for e in events))
writecsv(out/'rejected.csv',rejects);writecsv(out/'duplicates.csv',dups)
rules=json.loads((data/'rules.json').read_text());cutoff=parse_time(rules['cutoff'])
by_order=defaultdict(list)
for e in events:by_order[e['order_id']].append(e)
orders=readcsv(data/'orders.csv')
records=[classify_order(o,by_order[o['order_id']],cutoff) for o in orders]
writecsv(out/'order-review.csv',records)
hubs=aggregate(records,['hub']);tiers=aggregate(records,['hub','service']);routes=aggregate(records,['route'])
writecsv(out/'hub-metrics.csv',hubs);writecsv(out/'route-metrics.csv',routes)
bands,std=fairness(tiers,rules)
writecsv(out/'service-review.csv',bands);writecsv(out/'standardized-comparison.csv',std)
findings=investigate(readcsv(data/'access-log.csv'),json.loads((data/'access-policy.json').read_text()))
writecsv(out/'security-findings.csv',findings)
summary=dict(orders=len(orders),accepted_events=len(events),rejected=len(rejects),duplicates=len(dups),cohorts=dict(Counter(r['cohort'] for r in records)),hubs=hubs,service_review=bands,standardized=std,security=dict(flagged_requests=len(findings),potential_export_rows=sum(f['rows'] for f in findings if 'unauthorised_export' in f['finding']),potential_export_bytes=sum(f['bytes'] for f in findings if 'unauthorised_export' in f['finding']),findings=findings))
dump(out/'results.json',summary)
return summary
if __name__=='__main__':
import sys
print(json.dumps(solve(sys.argv[1],sys.argv[2]),ensure_ascii=False,indent=2))
大量请求本身不是越权证据,HTTP 401 也不是泄露成功证据。
每项 20 分,14 分及以上且没有关键口径错误再进入下一次 Review。原始数据被改写、静默丢记录、分母含未到期、把 401 算成泄露、用小样本硬排站点或把前端筛选当授权,均须修正后再验收。
- 策略与会话判断 6 分
- 证据请求可追溯 5 分
- 反证与影响量不夸大 5 分
- 处置与验证可执行 4 分