关于 audit-permissions
audit-permissions 是用于 Power Pages 网站的审计技能,会将表权限与网站代码及 Dataverse 元数据对照,并生成含评分、发现和修复建议的 HTML 报告。其固定规则让相同问题集得到一致评分。
主要功能
- 01对照代码和 Dataverse 元数据检查权限
- 02运行 A–K 表级审计清单
- 03按固定规则计算类别评分
- 04归并根因并标注问题来源
- 05生成并验证 HTML 报告
适用场景
- 01查找缺失或过度配置的 CRUD 权限
- 02检查关联、展开和父级权限链
- 03为权限审查生成可复现的评分
- 04汇总问题证据与修复建议
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ microsoft/ power-platform-skills --skill audit-permissions安装后在 Claude Code 中输入/audit-permissions确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/audit-report.htmlreferences/non-spa-site-workflow.mdscripts/collect-non-spa-permission-evidence.jsquery-table-lookups.jsquery-table-relationships.jsvalidate-audit.js详细介绍
插件检查:运行
node "${PLUGIN_ROOT}/scripts/check-version.js"——如果输出消息,请在继续前展示给用户。
审计权限
审计 Power Pages 代码站点上的现有表权限。根据站点代码和 Dataverse 元数据分析权限,然后生成可视化 HTML 报告,其中包含确定性的 1–5 类别评分、建议性判定、发现、推理过程和建议的修复方案。
此技能分为两部分:
- 审计检查(证据 + 修复方案)——逐表执行 A–K 检查清单,根据实际代码用法和 Dataverse 元数据核对每项权限。这是主要问题检测器:它会发现有具体代码依据的问题(缺少 CRUD、append/appendto、
$expand缺口、父级链断裂、过度授权),并将每项问题标记为主要或次要,同时提供具体证据和修复方案。 - 评分(可复现结果)——采用3 类别评分标准(9 个维度),以包含固定检测触发条件 + 严重程度的常见问题目录为依据;随后进行一次汇总,归纳根本原因,并对主要问题执行一次批量根因归因。该评分标准会将发现转换为纯公式计算的 1–5 类别评分,确保同一配置的两次运行得出相同结果。
评分黄金法则:你只需发现并标记问题(
major/minor)。切勿自行编造数字评分。1–5 分由根据问题数量应用的固定公式得出(参见附录 A)。问题集合相同 ⇒ 评分相同。
站点类型路由
阅读或执行下方工作流前,请先执行现有的 SPA 项目查找。
-
使用现有的文件搜索模式,在当前目录或其直接子目录中查找
powerpages.config.json。 -
如果找到,请继续执行下方现有的 SPA 工作流。
-
只有当现有 SPA 查找未找到
powerpages.config.json时,才运行:node "${PLUGIN_ROOT}/scripts/lib/detect-project-context.js" -
如果返回
siteType: "non-spa",请阅读并遵循${PLUGIN_ROOT}/skills/audit-permissions/references/non-spa-site-workflow.md。 不要执行下方的 SPA 工作流。 -
否则,请继续执行步骤 1,并保留现有的“未找到”行为。
工作流
- 验证站点部署——检查
.powerpages-site文件夹和表权限是否存在 - 收集配置——读取 Web 角色、表权限和站点代码,运行共享架构验证器,并构建确定性的预检交叉引用
- 分析与发现——使用确定性脚本查询 Dataverse 中的关系和查找列
- 运行审计检查——根据代码用法和最佳实践核对权限(A–K 检查清单)
- 为配置评分——使用常见问题目录,按 3 个类别检测并标记问题;将问题归并为不同的根本原因(主要/次要),计算 1–5 类别评分并给出判定
- 根因归因——通过一次批量处理,将每项主要问题标记为仅与权限有关,或由上游产物导致,并在报告中附上一句说明
- 生成报告——创建 HTML 报告(评分概览 + 发现 + 清单),并进行验证
- 呈现发现并跟进——总结结果、记录技能使用情况,并询问用户是否要修复问题
**重要:**分析期间不要向用户提问。请自主收集所有数据,然后呈现发现结果。
审计完成遥测
在第 1 步一开始、验证站点之前,初始化本地运行状态:
node "${PLUGIN_ROOT}/scripts/emit-audit-permissions-telemetry.js" --action start
解析 JSON 标准输出,并在本次调用中将其 auditRunId 保留为 <AUDIT_RUN_ID>。此命令不会发出遥测事件;现有的通用技能调用事件就是开始信号。本地运行状态提供完成遥测所需的不透明随机 ID 和开始时间。切勿从站点、环境、租户、用户、路径或报告值推导或替换该 ID。
遥测为尽力而为,绝不能阻塞审计。如果初始化命令失败或未返回 auditRunId,则继续审计,不发送完成遥测。不要编造 ID,也不要在本地状态中搜索最近一次运行。
在返回运行 ID 后发生任何可控的终止性失败时,停止前先结束本次运行:
node "${PLUGIN_ROOT}/scripts/emit-audit-permissions-telemetry.js" \
--action complete \
--runId "<AUDIT_RUN_ID>" \
--outcome failure \
--failureStage "<FAILURE_STAGE>"
只能使用以下固定失败阶段之一:
site_verificationconfiguration_gatheringschema_validationrelationship_discoveryaudit_checksscoringroot_cause_attributionreport_renderingreport_validationmetrics_validationunknown_controlled_failure
不得向遥测脚本传递错误消息、路径、表名、发现内容或其他用户/站点数据。遥测完成失败不会改变审计结果,也不得触发第二次完成尝试。
任务跟踪
在第 1 步开始时,使用 TaskCreate 一次性创建所有任务。开始时将每项任务标记为 in_progress,完成时标记为 completed。
| 任务主题 | activeForm | 说明 |
|---|---|---|
| 验证站点部署 | 正在验证站点部署 | 检查是否存在 .powerpages-site 文件夹和表权限 |
| 收集配置 | 正在收集配置 | 读取 Web 角色、表权限和站点代码;运行验证器;构建预检 |
| 发现关系 | 正在发现关系 | 查询 Dataverse,查找查找列和关系 |
| 运行审计检查 | 正在运行审计检查 | 为每张表创建任务并运行检查清单(A–K),然后进行交叉验证 |
| 评估配置 | 正在评估配置 | 通过重复问题目录检测/标注 3 个类别,归并为根因,并计算 1–5 分 |
| 归因根因 | 正在归因根因 | 一次性将每个主要问题标记为仅权限问题或上游原因导致 |
| 生成报告 | 正在生成报告 | 创建并验证 HTML 报告,并在浏览器中显示 |
| 呈现发现 | 正在呈现发现 | 总结结果、记录使用情况,并提出修复问题 |
注意:“运行审计检查”阶段会在第 4.2 步动态创建额外的逐表任务。这些逐表任务用于独立跟踪每张表的系统性 A–K 检查清单。
第 1 步:验证站点部署
使用 Glob 查找:
**/powerpages.config.json— 用于识别项目根目录**/.powerpages-site/table-permissions/*.tablepermission.yml— 现有权限
如果不存在 .powerpages-site 文件夹,则停止并告知用户先使用 /deploy-site 进行部署。
如果不存在表权限,则将此记录为关键发现(站点可能未配置任何数据访问权限),并继续审计——代码中可能仍有需要配置权限的引用。
因缺少 .powerpages-site 而停止之前,按照上述说明使用 failureStage: site_verification 结束完成遥测。
第 2 步:收集配置
单次遍历性能约定
在作出判断前,通过一次有界遍历收集所有本地证据:
- 通过分组操作读取 Web 角色、表权限、站点设置和
.datamodel-manifest.json。 - 只扫描一次源文件,查找 API 操作、实体集、选择项、筛选器、查找项、扩展项、上传、路由、导入以及功能/身份验证门控。优先使用一次分组搜索,再有针对性地读取仅包含匹配内容的区域;切勿逐个读取整个源代码树中的文件。
- 仅定位一次权限计划。在一次操作中提取所有与授权相关的语句到
planClaims;之后不要重新打开或重新提取该计划。 - 将这些事实固定记录在
audit-evidence.json中。步骤 4–6 只使用此记录以及任何通过门控的 Dataverse 结果。
不要为了提高把握而重新扫描源代码或计划。只有在记录中缺少特定字段,且已知所需的准确文件/区域时,才允许再次读取。将新增证据记录一次,然后继续。切勿针对同一计划声明进行多次不同措辞的搜索。
2.1 读取 Web 角色
读取所有匹配 **/.powerpages-site/web-roles/*.yml 的文件。键可能带有发布者前缀——依次尝试 mspp_<key>、adx_<key>,然后尝试不带前缀的 <key>(以首次找到的为准)。从每个文件中提取 id、name、anonymoususersrole、authenticatedusersrole。如果 YAML 解析失败或其根节点不是对象,则记录解析错误(用于预检 IS)。
2.2 读取表权限
读取所有匹配 **/.powerpages-site/table-permissions/*.tablepermission.yml 的文件。对于每项权限,使用相同的前缀规则提取:
entityname(权限名称)entitylogicalname(表)scope(数值代码)→ 映射为标签:756150000 = Global、756150001 = Contact、756150002 = Account、756150003 = Parent、756150004 = Self;其他值 →Unknown(<code>);缺失 → scope-1,标签Unknownread、create、write、delete、append、appendto(布尔标志)adx_entitypermission_webrole(Web 角色 UUID 数组)→ 通过 §2.1 将每个 GUID 解析为角色名称;无法解析 → 标记为<unresolved:…>contactrelationship、accountrelationship(如果作用域为 Contact/Account)parententitypermission、parentrelationship(如果作用域为 parent)
解析失败时记录解析错误(用于预检 IS)。
2.3 分析站点代码
在唯一一次源代码遍历中,搜索站点源代码中的:
- Web API 调用(
/_api/) - 查找项绑定(
@odata.bind) - 文件上传(
uploadFileColumn、uploadFile、upload*Photo、upload*Image) $expand用法($expand、buildExpandClause、ExpandOption)
同时检查项目根目录中是否存在 .datamodel-manifest.json,以获取权威表清单。其格式为 { environmentUrl?, tables: [{ logicalName, displayName, status?, columns: [{ logicalName, type }] }] }。如果文件缺失或 tables 不是数组,则视为数据模型缺失。
此外,还需扫描 Web API 工件(用于步骤 6 中的根因归属分析,尽力而为):
- 中央客户端:如果
src/shared/powerPagesApi.ts(x)或src/lib/powerPagesApi.ts中任一文件存在,则标记为存在。 - 服务文件:在
src/shared/services/**下(.ts/.tsx/.js/.jsx)记录每个文件中出现的数据模型表逻辑名称和 HTTP 方法(GET/POST/PATCH/PUT/DELETE)。 - 通配符列选择:记录所有已扫描源文件中每次出现的
$select=*(或值为*的$select),以及对应的文件路径和所查询的表。 - 类型文件:在
src/types/**下提取第一个interface/type字面量的属性名称;尽可能根据文件名/内容推断表。 - Web API 站点设置:在
.powerpages-site/site-settings/中(文件名为Webapi-*/Webapi/*)解析name+value。按/拆分name:Webapi/<entitySet>/enabled→ 类型为enabled;.../fields→ 类型为fields;Webapi/error→ 类型为error;否则为other。webapiEnabledEntitySets=enabled值恰好为"true"的实体集。对于每个fields设置,还记录isWildcard——当去除首尾空格后的值为*,或任一逗号分隔项为*时为 true。列出列逻辑名称的列表无论多长,都不属于通配符。
还要扫描 .powerpages-site/site-settings/ 中的身份验证/注册站点设置,不论文件名为何。解析 Authentication/Registration/ 下设置的 name + value,包括 Enabled、OpenRegistrationEnabled、ExternalLoginEnabled 和 InvitationEnabled。还要解析每个外部提供程序的 Authentication/<Type>/<Provider>/RegistrationEnabled;如果某个提供程序已配置(存在 Authentication/<Type>/<Provider>/ 下的任意设置),但没有该项设置,则视为 true,这是服务器默认值。比较名称时不区分大小写;仅当去除首尾空格后的值不区分大小写等于 "true" 时,才视为已启用。缺失的设置记录为 unknown,而不是 false。
在判断权限之前,先构建受众可达性画像:
authenticatedusersrole: true的角色会自动分配给每位已登录的网站用户;这是一个广泛的基线角色,不能证明用户属于员工、教职工、教师、家长或其他可信用户画像。- 当
OpenRegistrationEnabled为true,且本地注册已启用(Authentication/Registration/Enabled为true),或ExternalLoginEnabled不为false(缺失时按服务器默认值视为true),并且至少有一个已配置的外部提供程序的RegistrationEnabled为true,即可证明开放注册已启用。启用开放注册后,任何访客都可以创建账户并进入广泛的 Authenticated Users 受众。 - 外部登录或邀请启用可能提供额外的入口,但没有支持配置证据时,不要假定公众可以使用这些入口。
- 在每项表范围和受众检查中都要纳入此画像。不要脱离用户获取角色的方式,孤立地评估角色名称或权限。
构建一份映射,列出:代码中引用了哪些表;每个表执行了哪些 CRUD 操作;使用了哪些查找关系;以及通过 $expand 获取了哪些关联表(这些表也需要读取权限)。
在判断访问权限缺失之前,还要构建一份能力矩阵。每项与授权相关的能力占一行:
persona | route/component | reachability | table | operation | current grant | plan claim | evidence
按以下规则机械地分类 reachability:
reachable——页面/组件已注册到已部署的路由器中,或由其他可达组件调用;并且没有显式功能标志或授权门禁针对该用户画像将其禁用。disabled——显式的 false 功能标志、服务器端授权门禁、构建排除项,或未注册/不可达的组件阻止了该能力运行。ambiguous——代码存在,但无法确定其是否可达或是否被排除。 基于已登录用户自身数据的 UI 条件(例如isAccountAdmin字段或其个人资料记录中的角色标记)永远不会让某项功能变为disabled——它用于识别使用该功能的用户角色。计划或注释中的“已延期”“未来阶段”或“尚不支持”等措辞,永远不会让已路由的功能变为disabled或ambiguous。
在整个审计中,始终遵循以下证据优先级:可访问的实现 > 明确的运行时门控/构建排除 > 实施计划 > 注释/名称。计划不能仅仅因为把使用该功能的角色称为“后台人员”,就让已路由且可调用的功能变成假设性功能。反过来,未使用、未注册、明确禁用或受服务器门控的代码,不能证明需要门户权限。如果可访问性存在歧义,且与计划冲突,则最多针对该冲突发出一项轻微的 internal-consistency 问题;不得推定流程已损坏。
完成源码检查后,评分期间不得重新检查路由、页面、服务或权限计划。可访问性和计划声明均为已冻结的台账事实,不是第 5 步中需要重新考虑的问题。
2.4 运行共享架构验证器
针对现有站点运行共享验证器:
node "${PLUGIN_ROOT}/scripts/validate-permissions-schema.js" --projectRoot "<PROJECT_ROOT>"
解析 JSON 输出,并将发现的问题作为重大/轻微问题纳入审计。映射如下:
error发现 → 重大问题warning发现 → 重大问题(纯前瞻性/表面问题除外,此类问题归为轻微)info发现 → 轻微问题
即使后续代码/Dataverse 分析也发现了其他问题,这些发现仍应纳入最终报告。例外:如果父级链有效,则忽略验证器针对 Parent 作用域子权限发出的“无关联 Web 角色”警告(参见检查 B)。
如果验证器报告 Webapi/<table>/fields 设置中存在通配符字段访问,则将此发现作为一项重大 security-posture 问题(每个受影响的设置各一项):
- 标题:
Unsupported wildcard Web API fields for <table> - **原因:**该字段设置使用通配符访问;此方式自 2026 年 9 月 14 日起不受支持
- **修复:**使用
${PLUGIN_ROOT}/references/webapi-field-allowlist.md替换通配符:普通列使用 LogicalNames,查找读取使用_<LogicalName>_value,@odata.bind使用确切的 Navigation Properties - **详情:**包含验证器发现中的站点设置文件路径和设置名称
第 3.1 步确定环境 URL 后,仅当第 3 步的发现门控已开启时,才使用实时关系验证重新运行共享验证器。合并所有新增发现:
node "${PLUGIN_ROOT}/scripts/validate-permissions-schema.js" --projectRoot "<PROJECT_ROOT>" --validate-dataverse-relationships --envUrl "<envUrl>"
仅在需要关系证据的本地运行中使用此 Dataverse 支持的关系验证。CI 或其他离线环境中不需要此验证;对于没有依赖关系的可访问流程的仅 Global/只读设计,也不要运行此验证。
2.5 确定性预检交叉检查(评分器基准事实)
对解析后的 YAML、数据模型和 Web API 产物进行纯机械交叉检查。输出所有适用项。 按遇到顺序为每个前缀编号(IS1、IS2、DM1……)。这些是基准事实:将它们纳入第 5 步相应维度的问题列表,绝不能遗漏或降级。
| 条件 | ID 前缀 | 维度 | 严重程度 |
|---|---|---|---|
| Web 角色 YAML 解析错误 | IS | 内部一致性 | 主要 |
| 表权限 YAML 解析错误 | IS | 内部一致性 | 主要 |
| 权限引用了任何角色中都未定义的 Web 角色 GUID | IS | 内部一致性 | 主要 |
| Web 角色的 id 为空 | IS | 内部一致性 | 主要 |
权限的 entitylogicalname 为空 | DM | 数据模型对齐 | 主要 |
| 权限未授予任何特权(6 个 CRUD 位全为 false) | TC | 表覆盖率 | 主要 |
权限没有范围(scope < 0) | SC | 范围正确性 | 主要 |
权限范围代码无法识别(Unknown(code)) | SC | 范围正确性 | 次要 |
| 权限向 Anonymous 角色授予写入/创建/删除权限 | AH | 匿名访问卫生 | 主要 |
| 权限目标表不在数据模型中(仅当存在有效的数据模型清单时) | DM | 数据模型对齐 | 主要 |
| Web API 站点设置 YAML 解析错误 | IS | 内部一致性 | 主要 |
-enabled 实体集没有匹配的 -fields 白名单 | IS | 内部一致性 | 主要 |
-fields 白名单没有匹配的 -enabled 设置 | IS | 内部一致性 | 主要 |
| 声明了数据模型中不存在的 1–6 个属性的类型文件(仅当存在有效的数据模型清单时) | DM | 数据模型对齐 | 次要 |
2.6 冻结证据台账
在网站中工作时,<OUTPUT_DIR> 为 <PROJECT_ROOT>/docs/(即包含 powerpages.config.json 的项目根目录);否则为系统临时目录。临时文件和最终报告都写入该目录。
在运行基于判断的检查之前,将迄今收集的规范化事实写入临时文件 <OUTPUT_DIR>/audit-evidence.json,包括角色、权限、数据模型表/列、服务操作、路由/组件可达性、能力矩阵、lookup/expand 使用情况、客户端行筛选、实施计划声明、身份验证可达性和预检问题。按稳定标识符对表、权限、路由和能力排序。
步骤 4–6 必须依据此台账进行分析,而不是重新扫描并选择性地重新发现证据。如果步骤 3 中的实时 Dataverse 探索添加了事实,则追加这些事实,并在步骤 4 之前将台账重写一次。评分期间,除非向台账添加了新证据,否则不得更改 reachability 分类。最终 HTML 报告通过验证后,删除 audit-evidence.json。
第 3 步:分析与探索(Dataverse API)
所有 Dataverse API 调用均使用确定性的 Node.js 脚本。这些脚本以一致的方式处理身份验证令牌获取、HTTP 请求和 JSON 解析。
Dataverse 发现门控
在调用 pac env who 或任何 Dataverse 查询脚本之前,计算 needsLiveDiscovery。仅当至少一项可访问的功能存在以下未解决依赖时,它才为 true:
- Contact、Account 或 Parent 范围,其关系名称必须经过验证
- 会绑定或更改查找列的创建/写入代码
$expand或相关表访问,其目标无法通过本地元数据确定- 关系或目标元数据尚未确定的文件/图像上传
如果以上依赖均不适用,则为 false——例如,仅限 Global 范围的只读工作流、可访问的网站代码未使用的显式 Administrator-only CRUD,以及已由清单和本地源代码完全确定的情况。为 false 时,跳过第 3 步的全部内容,在证据台账中记录 liveDiscovery: "not-needed",然后继续第 4 步。为 true 时,先确认当前活动 Dataverse 环境 URL 与清单中的环境 URL 一致;如果不一致,则跳过实时查询并记录不匹配情况,不要查询错误的环境。
3.1 获取环境 URL
pac env who
提取 Environment URL(例如 https://org12345.crm.dynamics.com),并在后续脚本调用中将其用作 <envUrl>。
3.2 查询查找列
对于启用了 create 或 write 权限的每个表,使用查找列查询脚本:
node "${PLUGIN_ROOT}/skills/audit-permissions/scripts/query-table-lookups.js" --envUrl "<envUrl>" --table "<table_logical_name>"
该脚本会为每个查找列返回一个 { logicalName, targets } JSON 数组。请保存此输出,用于构建下文所述的映射。
查询完所有具有 create 或 write 权限的表后,根据合并后的结果构建两个映射:
- 源映射(表 → 查找列):记录每个已查询的表所拥有的查找列及其目标。在 H2 节中用于检查源表上的
appendto。 - 反向目标映射(目标表 → 源表列表):对于任意查找列
targets数组中出现的每个目标表,记录引用它的源表。在 H 节中用于检查目标表上的append。
示例:查询 order_item 会返回 [{ logicalName: "cr4fc_orderid", targets: ["cr4fc_order"] }]
- 源映射:
order_item → [{ column: "cr4fc_orderid", targets: ["cr4fc_order"] }] - 反向目标映射:
cr4fc_order → [{ sourceTable: "order_item", column: "cr4fc_orderid" }]
H 节和 H2 节都会使用这两个映射:
- 源表(包含查找列)需要
appendto: true——它会链接到其他记录(通过源映射检查) targets中的每个目标表都需要append: true——其他记录会链接到该表(通过反向目标映射检查)
3.3 查询关系
对于具有 Parent 范围权限的表,使用关系查询脚本验证关系名称:
node "${PLUGIN_ROOT}/skills/audit-permissions/scripts/query-table-relationships.js" --envUrl "<envUrl>" --table "<parent_table>"
该脚本会返回一个 { schemaName, referencedEntity, referencingEntity, referencingAttribute } JSON 数组。使用 schemaName 验证 Parent 范围权限中的 parentrelationship 值。
错误处理
如果任何脚本以代码 1 退出,则跳过依赖 API 的检查,并在报告中注明跳过了哪些检查。遇到身份验证错误时,不要停止整个审计。使用数据模型清单和代码分析作为备用方案。
第 4 步:运行审计检查
使用按表任务跟踪,系统地运行每一项审计检查。这些基于代码和 Dataverse 的检查是主要的问题检测手段——它们能发现具体问题(缺少 CRUD、append/appendto、$expand 缺漏、Parent 链断裂、授权过度),仅凭规则判断则可能遗漏这些问题。这正是问题集更丰富、评分更准确的原因。每项未通过的检查都会产生一个待定候选项(标题、负责的表、证据和建议的修复方法);第 5 步会为其指定最终的主要/次要严重性和维度。通过的检查不会产生候选项。如果某项检查要求搜索服务代码,请先在证据台账(第 2.6 步)中查找相关信息;仅当台账中没有时才搜索源代码。
4.1 构建审计清单
首先,从两个来源构建要审计的所有表的合并列表:
- 代码中引用的表(来自步骤 2.3)——这些表可能有权限,也可能没有
- 已有权限的表(来自步骤 2.2)——这些表可能在代码中被引用,也可能没有
这两组表的并集就是完整的审计范围。每张表都要从两个方向进行审计:“代码是否需要但尚不存在的权限?”以及“权限是否与代码实际执行的操作相符?”
4.2 创建逐表审计任务
为审计清单中的每张表创建一个任务:
TaskCreate:
subject: "Audit <table_logical_name>"
activeForm: "Auditing <table_display_name> permissions"
description: "Run all audit checks for <table_logical_name>"
还要创建一个汇总任务:
TaskCreate:
subject: "Compile audit findings"
activeForm: "Compiling audit findings"
description: "Combine all per-table findings into the final report"
可随时使用 TaskList 查看进度,并确认哪些表仍需审计。
4.3 逐表审计检查清单
对每张表,将其任务标记为 in_progress,并按顺序完成以下检查。对于发现的每个问题,记录支持该问题的具体证据(文件路径、权限名称、代码模式)。不适用于此表的检查可跳过。
不要在步骤 4 中确定严重性或维度。 以下每项检查都包含一个暂定审计等级(critical、warning 或 info),并生成一个事实性候选项,格式为 { sourceId, auditTier, title, table, reasoning, fix, evidence, reachability? }。检查通过时不生成候选项。步骤 5 是唯一分配最终 major|minor 严重性和规范维度的步骤;分配时需先应用固定目录规则,再应用通用策略。这样可以避免先运行的审计路径锁定不同的评分。
A. 权限是否存在
此表是否有表权限?
- 如果代码中引用了此表,但此表没有权限 → 发现项:
- 严重性:
critical - 标题:
Missing permission for <table> - 原因: 指出引用此表的代码文件及其执行的操作
- 修复: 创建具有适当范围和 CRUD 标志的权限
- 严重性:
- 如果权限存在,但代码中没有引用此表 → 发现项:
- 严重性:
info - 标题:
Unused permission for <table> - 原因: 任何源代码中都未引用此表——此权限可能不再需要
- 修复: 检查此权限是否仍有必要
- 严重性:
- 如果两者都存在 →
pass,继续其余检查
B. Web 角色关联
此权限是否分配了 Web 角色?
- 检查
adx_entitypermission_webrole——如果为空或缺失 → 发现项:- 严重性:
warning - 标题:
Permission <name> has no web role association - 原因: 未关联 Web 角色的权限不会生效——没有用户会获得此访问权限
- 修复: 关联适当的 Web 角色
- 严重性:
- 例外:父级范围(
756150003)的子权限可以不设置角色——它会通过父级链继承角色关联。如果其parententitypermission指向现有权限,则不报告发现项;如果链路断裂,则由检查 I 报告。 - 如果已分配角色 →
pass
C. 范围是否恰当
此范围是否是在满足需求的前提下权限最低的选项?
- 搜索服务代码中的范围相关模式:联系人范围筛选器(
getCurrentContactId、_contactid_value、contactid)和账户范围筛选器(_accountid_value、parentcustomerid) - 针对每个全局读取权限,解析所有已分配的角色,并应用第 2.3 步的受众可达性配置文件。然后检查表中选定/列入白名单的列、数据模型列、UI 用途和实施计划,判断数据是否明确公开,或是否仅面向范围更窄的用户角色。
- 强制跨构件触发条件,针对每个受影响的表单独评估: 当以下 4 个条件全部满足时,生成一条发现:(1) 已证实开放注册(第 2.3 步受众可达性配置文件);(2) 已分配的 Web 角色设置了
authenticatedusersrole: true,因此每个注册账户都会自动获得该角色;(3) 该角色已关联到此表的权限,且read: true、范围为 Global;(4) 该表包含个人、名册、成绩、联系人或内部运营数据,或者站点意图将其限制为员工/教师/其他特定用途的用户角色 → 发现:- 严重性:
critical - 标题:
自助注册用户可以读取所有 <table> 记录 - 原因: 说明完整的证据链:注册设置 → 自动加入 Authenticated Users → 权限角色绑定 → Global 范围 → 暴露的列/预期受众。仅有身份验证不能证明用户属于可信用户角色。
- 修复: 从此权限中移除基线 Authenticated Users 角色,并改为分配仅向经过验证的用户配置的特定用途角色;若访问权限应基于记录关联,则使用 Contact、Account、Parent 或 Self 范围,而非 Global。
- 严重性:
- 如果 Global 范围(
756150000)启用了write或delete→ 发现:- 严重性:
warning - 标题:
<table> 的 Global 范围启用了写入/删除 - 原因: 任何拥有此角色的用户都可以修改/删除此表中的任意记录
- 修复: 缩小到 Contact 或 Account 范围;如果不需要,则移除写入/删除权限
- 严重性:
- 如果 Global 范围仅启用
read,只有在表及暴露的列明确为公开/非敏感,且所有已分配角色均属于预期受众时才通过。否则,根据上述强制触发条件和反复出现的问题 C3/C4 进行评估;只读访问仍可能造成严重的机密信息泄露。 - 如果代码使用联系人范围筛选器,但权限使用 Global → 发现:
- 严重性:
warning - 标题:
<table> 的范围可以更窄 - 原因: 代码按当前联系人筛选,但权限授予 Global 访问权限
- 修复: 缩小到 Contact 范围
- 严重性:
- 否则 →
pass
D. 读取权限
read 设置是否正确?
- 搜索服务代码中对此表的 GET/列表/获取模式:对
/_api/<entity_set>的 API 调用、列表/获取函数(list<TableName>、get<TableName>) - 如果代码读取此表,但
read: false→ 发现:- 严重性:
critical - 标题:
<table> 缺少读取权限 - 原因: 代码会读取此表,但权限未授予读取访问权限
- 修复: 启用
read: true
- 严重性:
- 如果
read: true且代码会读取 →pass
E. 创建权限
create 设置是否正确?
- 搜索服务代码中的 POST/创建模式:POST 方法用法(
method: 'POST')、创建函数(create<TableName>) - 如果代码会创建记录,但
create: false→ 发现:- 严重性:
critical - 标题:
<table> 缺少创建权限 - 原因: 代码会在此表中创建记录,但权限未授予创建访问权限
- 修复: 启用
create: true
- 严重性:
- 如果
create: true,但代码中没有创建模式 → 发现:- 严重性:
info - 标题:
<table> 已启用创建权限,但未使用 - 原因: 在代码中未找到创建操作 — 权限可能过于宽松
- 修复: 如果不需要,请考虑禁用
create
- 严重性:
- 如果匹配 →
pass
F. 写入权限
write 设置是否正确?
- 在服务代码中搜索 PATCH/更新/上传模式:PATCH 方法用法(
method: 'PATCH')、更新函数(update<TableName>)、文件上传模式(uploadFileColumn、uploadFile、upload*Photo、upload*Image、upload*File) - 如果代码会更新记录,但
write: false→ 发现项:- 严重性:
critical - 标题:
<table>缺少写入权限 - 原因: 代码会更新此表中的记录(或上传文件),但权限未授予写入访问权限
- 修复: 启用
write: true
- 严重性:
- 如果发现文件上传模式,但
write: false→ 发现项:- 严重性:
warning - 标题: 检测到文件上传,但
<table>的写入权限已禁用 - 原因: 文件上传使用 PATCH,需要写入权限
- 修复: 启用
write: true
- 严重性:
- 如果
write: true,但read: false→ 发现项:- 严重性:
warning - 标题:
<table>已启用写入,但未启用读取 - 原因: 用户可以修改自己无法查看的记录,这种情况不常见,且可能并非本意
- 修复: 启用
read: true
- 严重性:
- 如果
write: true,但代码中没有写入模式 → 发现项:- 严重性:
info - 标题:
<table>已启用写入,但未使用 - 原因: 代码中未发现更新操作 — 此权限可能过于宽松
- 修复: 如果不需要,请考虑禁用
write
- 严重性:
- 如果匹配 →
pass
G. 删除权限
delete 设置是否正确?
- 在服务代码中搜索 DELETE 模式:DELETE 方法用法(
method: 'DELETE')、删除函数(delete<TableName>) - 如果代码会删除记录,但
delete: false→ 发现项:- 严重性:
critical - 标题:
<table>缺少删除权限 - 原因: 代码会删除此表中的记录,但权限未授予删除访问权限
- 修复: 启用
delete: true
- 严重性:
- 如果
delete: true,但代码中没有删除模式 → 发现项:- 严重性:
info - 标题:
<table>已启用删除,但未使用 - 原因: 代码中未发现删除操作 — 此权限可能过于宽松
- 修复: 如果不需要,请考虑禁用
delete
- 严重性:
- 如果匹配 →
pass
H. Append(目标表检查)
此表是否需要 append: true?目标表必须启用 Append — 其他记录通过查找列链接到的表。
- 检查第 3.2 步中的反向目标映射:是否有其他具有
create或write权限的表将此表作为查找目标? - 还要在服务代码中搜索对此表实体集的
@odata.bind引用(例如/<entity_set>() - 如果此表出现在反向目标映射中(即,具有
create/write权限的另一张表包含指向此表的查找),但append: false→ 发现项:- 严重性:
critical - 标题:
<table>缺少 Append 权限 - 原因: 表
<source_table>的查找列<column>指向此表,并在创建/写入时设置该列的值。目标表需要 Append 权限,以便记录可以关联到该表。用户会看到“You don't have permission to associate or disassociate” - 修复: 启用
append: true
- 严重性:
- 如果
append: true且有依据 →pass - 如果
append: true,但此表未出现在反向目标映射中,且代码中也没有将其作为查找目标的引用 → 发现项:- 严重性:
info - 标题:
<table>已启用 Append,但并非必需 - 原因: 没有其他具有
create/write权限的表包含指向此表的查找 — Append 可能不需要 - 修复: 如果不需要,请考虑禁用
append
- 严重性:
H2. AppendTo(源表检查)
此表是否需要 appendto: true?源表必须启用 AppendTo — 即包含指向其他记录的查找列的表。
- 查看 Step 3.2 中的源映射:此表是否有查找列?
- 另外,搜索服务代码,查找此表创建/更新调用中的
@odata.bind模式 - 如果此表在源映射或代码中有查找列,且启用了
create或write,但appendto: false→ 发现问题:- 严重性:
critical - 标题:
Missing appendto on <table> - 原因: 此表在创建/写入时设置了指向
<target_table>的查找列<column>,这需要 appendto 权限。用户会看到“你没有关联或取消关联的权限” - 修复: 启用
appendto: true
- 严重性:
- 如果
appendto: true且理由充分 →pass
I. 父链完整性
如果权限的范围为 Parent(756150003):
- 验证
parententitypermission是否引用了一个有效且存在的权限 ID - 验证
parentrelationship是否为有效的 Dataverse 关系(如果 API 可用,请使用 Step 3.3 的结果) - 如果无效 → 发现问题:
- 严重性:
critical - 标题:
Broken parent chain for <permission> - 原因: 父权限引用无效 — 此权限不会授予任何访问权限
- 修复: 更正父权限 ID 和/或关系名称
- 严重性:
- 如果有效 →
pass
J. $expand 相关表覆盖
此表是否通过对另一个表的查询使用 $expand 获取?
- 查看 Step 2.3 中的
$expand分析(搜索站点源代码中的$expand、buildExpandClause、ExpandOption) - 如果从另一个表展开了此表,但同一 Web 角色没有此表的
read: true表权限 → 发现问题:- 严重性:
critical - 标题:
Missing read permission for expanded table <table> - 原因: 在
<service_file>中,此表通过对<parent_table>的$expand获取,但没有读取权限。Power Pages 会对查询中的每个实体强制执行表权限。 - 修复: 为同一 Web 角色创建
read: true的表权限。对于集合值展开(一对多),使用 Parent 范围和关系名称。对于单值展开(指向参考数据的查找),使用 Global 范围并仅授予读取权限。
- 严重性:
- 如果覆盖正确 →
pass
K. 记录发现并完成
完成所有检查后,通过 TaskUpdate 将此表的任务标记为 completed。
4.4 跨表验证
完成所有逐表审计后,运行以下跨表检查:
- Append/AppendTo 一致性: 使用 Step 3.2 中的源映射和反向目标映射进行验证:(a) 每个源表(具有查找列且支持创建/写入)都设置了
appendto: true;(b) 反向映射中的每个目标表都设置了append: true;(c) 没有表在源映射中不存在查找列却设置了appendto: true;(d) 没有表未出现在反向目标映射中却设置了append: true $expand覆盖: 验证每处$expand用法中展开的表都有read: true- 父链完整性: 验证每个 Parent 范围的权限都引用了存在且有效的父权限
- Web 角色一致性: 如果两个相关表(例如父表和子表)由同一功能访问,请验证它们分配了相同的 Web 角色
使用 TaskList 检查所有已完成的审计,然后将“Compile audit findings”任务标记为 in_progress,并继续执行 Step 5。
第 5 步:为配置评分
这是评分叠加层。你已经有证据:审计问题(第 4 步)和确定性预检问题(第 2.5 步)。现在将它们整理为按类别组织的问题集,并给出可复现的 1–5 分评分。审计检查提供大部分已检测到的问题;下方的重复问题目录以及评分器严重性策略(附录 C)用于确定最终的 major/minor;公式(附录 A)则决定分数。检测和评分按类别组织(每个类别包含 3 个维度——附录 A / 附录 B)。
5.1 按类别检测并标记问题(以目录为依据)
评估3 个类别;在每个类别中,综合考虑其 3 个维度。每个问题从以下三个来源中提取:
- 预检问题(第 2.5 步)——逐字保留每个问题的固定 ID、维度和严重性。
- 审计候选项(第 4 步,以及第 1 步中关于无权限的观察)——这些是事实观察,尚未完成最终标记。先将每个候选项与重复问题目录匹配。只有未匹配的候选项才使用通用的附录 C策略。第 5 步会为每个问题恰好分配一次最终严重性和维度。
- 重复问题目录 + 评分标准判断——将下方目录作为检查清单逐项核查,然后补充 A–K 检查项未涵盖的内容(例如,一个完全只读的管理站点 → 意图覆盖失败)。
重复问题目录——检测 + 严重性(确保每次运行结果一致性的主要手段)。 对照产物逐一检查每种模式;只要某种模式的触发条件成立,就在指定维度中为其提出且仅提出一个问题,并采用固定严重性——每次运行都如此,无论它看起来多么明显或轻微。根据所描述的形态进行匹配,而不是根据具体的表名/角色名进行匹配(括号中的名称仅作示例)。模式按受影响的表组分别评估,且彼此不互斥:如果一张表符合某个 MAJOR 模式,而另一张表符合相似的 MINOR 模式(例如,PII 表符合 C2,同时草稿内容表符合 C14 minor),则应分别提出两个问题——排序规则只用于解决同一表组内的严重性,不会抑制对另一张表的发现。
- [C1] 匿名写入、创建或删除——由预检
AH行(步骤 2.5)提出;此处不要重复提出。 - [C2] 匿名角色可读取 PII/联系信息列(电子邮件、电话、监护人或家长联系方式、地址)→ 维度
anonymous-access-hygiene· 重大。 - [C3] 某个角色拥有某个按个人/所有者划分且承载 PII 或敏感运营数据(个人记录、联系信息 PII、成绩、名册)的表的全局范围访问权限——可访问所有记录。可变更的授权 →
privilege-calibration;只读授权 →security-posture· 重大。这明确包括基线 Authenticated Users 角色:当开放注册允许任何访客获取该角色时,应将其视为广泛的外部受众,而非可信员工。对每个受影响的表分别评估并报告此模式;不要将 Student 和 Teacher(或其他互不重叠的表暴露)合并为同一个问题。 - [C4] 某个角色获得的访问权限(任何 CRUD)超出其自然所需知情/所需操作范围 → 维度
privilege-calibration· 仅当额外访问权限涉及其他受众的敏感记录(PII、机密、财务信息)时为 重大;否则为 轻微。C4 不适用于 C3、C9、C16、C17 或 C18 涵盖的情况。 - [C5] 网站需要创作/管理功能,但没有任何角色对任何表拥有创建/写入/删除权限(完全只读)——整个网站仅报告一次 → 维度
intent-coverage· 重大 (如果预期由有意省略的Administrators角色执行创作,则不要报告——参见下文 ADMINISTRATORS EXCEPTION)。 - [C6] 网站显然需要公开的表(由服务/UI 代码引用,或明显属于最终用户实体)没有任何权限授予所需访问权限 → 维度
table-coverage· 重大 (如果唯一缺失的授权是授予Administrators角色,则不要报告——参见 ADMINISTRATORS EXCEPTION)。 - [C7] 网站需要特定用途的角色人群(例如不同的学生/教师/员工/管理员受众),但只存在通用的 Anonymous/Authenticated 角色 → 维度
role-completeness· 重大 (缺少Administrators角色/角色人群不属于缺口——参见 ADMINISTRATORS EXCEPTION)。 - [C8] 已定义 Web 角色,但没有任何表权限引用该角色(孤立角色),而网站所需的每种角色人群仍由其他角色覆盖 → 维度
role-completeness· 轻微。 - [C9] 对固有公开/非敏感共享参考数据(公告、设施、公开的院系/领导名单)进行全局范围读取是正确的范围 → 无问题。在清单/上下文中记录公开受众,但不要降低安全评分或要求修复。
- [C10] 仅在假设的未来变更中才有意义的范围/权限(“如果监护人将来要进行身份验证,Contact 或 Parent 范围会更合适”)→ 维度
scope-correctness· 轻微。 - [C11] 不存在
permissions-plan.html,或计划内容仅为通用/模板化说明(“Global 是范围最广的选项”)→ 维度internal-consistency· 轻微。 - [C12] 数据模型中的某个表没有权限,且其面向用户的状态确实不明确——实现计划或数据模型将其描述为面向用户,但没有代码使用它(可能是计划中的功能从未实现)→ 维度
table-coverage· 轻微。显然属于后端/系统的表没有权限是正确的,因此不要报告问题。 - [C13] 通配符
*列暴露——Web API 查询使用$select=*,或任何其他列清单设为*→ 维度security-posture· 重大,每个受影响的表报告一个问题。Webapi/<table>/fields通配符应通过步骤 2.4 的验证器发现项报告。明确列出的列清单无论多长,都不属于此问题。 - [C14] 门户可读取的表同时包含允许和受限的行,而唯一的区分方式是客户端可控制的
$filter/查询谓词。客户端筛选不是授权边界。如果绕过后会暴露敏感或机密内容(PII、凭据/机密信息、财务/健康数据、内部员工或 Agent 备注、私人案件详情、安全数据)→ 维度security-posture· 重大。如果绕过后只会暴露非敏感内容,且这些内容仅为未发布、草稿或工作流尚未完成 → 维度security-posture· 轻微。如果 Dataverse 权限、列安全性、服务器端端点或独立的受保护表强制执行区分,则不触发 C14。 - [C15] 特定用途的角色人群至少有一项
reachable门户功能,但缺少该角色人群的 Web 角色以及跨该功能所涉表/操作所需的权限 → 仅报告 一个合并后的维度role-completeness· 重大问题。将由此产生的缺表和缺少操作权限症状合并到该问题中。如果角色人群的角色已存在,只是一个表未覆盖,则使用 C6table-coverage;如果表权限已存在,只是缺少一个操作位,则使用intent-coverage。disabled代码不会触发 C15。与计划冲突的ambiguous代码只产生步骤 2.3 中所述的轻微internal-consistency问题。 - [C16] 员工共享工作队列——员工/后台角色人群的角色(不是 Anonymous,也不是基线 Authenticated Users 角色)对共享工作队列表(案件、工单、请求、任务)或其子表拥有 Global 范围。如果该角色可访问的页面会读取或更新跨所有者的这些记录 → 无问题(不要报告 C3 或 C4)。没有任何可访问的员工页面会使用的 Global 写入/创建权限 → 并入该角色的 C18 问题(轻微)。没有任何可访问的员工页面会执行的 Global 删除权限 → 维度
privilege-calibration· 重大。Anonymous 或客户/基线角色的任何变更操作仍属于 C1/C3 重大问题。 - [C17] 子表权限一致性——子表权限(评论、附件、备注、升级记录)的可访问范围与同一角色在父表上的授权范围相同(例如 Global 父表下的 Global 子表,或其下的 Parent 范围)→ 无问题。子表授权范围大于该角色在父表上的授权范围 → 应用 C3(当子表包含敏感内容时为 重大)。
- [C18] 未使用的授权——没有任何可访问代码使用的角色权限(未使用的表、未使用的创建/写入/删除权限位、不需要的 append/appendto),包括明确授予 Administrators 的权限 → 每个角色仅报告 一个问题,维度
privilege-calibration· 轻微,列出所有未使用的授权。不要按表拆分。C16 中未使用的删除权限仍单独报告为 重大问题。 管理员例外(务必严格遵守)。 按照设计,站点不应为AdministratorsWeb 角色创建表权限——管理员创作通常在门户外进行(通过模型驱动应用或直接使用 Dataverse),因此无需显式授予 Administrators CRUD 权限,这只会增加安全噪声。AdministratorsWeb 角色不会绕过门户数据访问的表权限。因此: - 如果没有
reachable站点代码执行管理员操作,则不具备 Administrators 表权限是正确的——绝不能仅仅因为Administrators角色缺少表权限,就报告缺失权限、表覆盖范围、角色完整性或完全只读方面的问题。这包括仅因创作/CRUD 属于被有意省略的Administrators角色而产生的暴露不足问题(C5 只读站点、C6 缺少授权、C7 缺少角色)。 - 如果
reachable站点代码通过门户执行仅限管理员的 CRUD 操作(例如,路由到的管理员页面调用 Web API),但没有授权,则此例外不适用:应像对待任何其他角色一样报告 C6 或 C15。 - 此例外仅适用于
Administrators角色,且仅适用于暴露不足/可用性问题。它绝不会降低过度暴露(安全)问题的等级——如果 Administrators(或任何角色)实际持有过于宽泛的授权,应照常按安全规则评分。
新问题(C1–C18 未涵盖):仍需检测——不要将范围局限于问题目录。按照 附录 C 中以安全为先的策略分配严重程度。新问题不会自动归为轻微问题:如果它将数据暴露给不应访问的受众、授权范围过宽或导致必要流程中断,则属于重大问题。
按规范维度前缀(IC、PC、SC、RC、TC、AH、DM、IS、SP)为基于 LLM 的问题 ID 编号。在每个维度内,接续该前缀已使用的最高预检 ID;若没有,则从 1 开始,以确保生成的 ID 不会与固定预检 ID 冲突。问题 ID 前缀必须与合并后问题的最终维度一致。每个问题记录为 {id, dimension, severity, title, reasoning, fix},并附上支持该问题的证据——即报告发现项的结构(第 7.2 步)。不要分配按维度划分的 1–5 分数——应在合并后按类别评分。
5.2 合并为不同的根本原因(major/minor)
将汇总后的完整问题列表合并为不同的根本原因,然后按主要影响将每个问题归入且仅归入一个类别。完成此步骤后,问题数量等于根本原因数量。
- 将描述同一受众在同一张或多张表上相同底层配置错误的问题合并为一个问题——即使它们属于不同维度/类别。不要让宽泛描述(“所有表均为 Global”)吞并不同表上的独立暴露;C3 问题仍须每张受影响的表各占一个问题。在描述中列出所有影响;合并后的严重性设为其中最高的级别。
- 每个根本原因只归入一个类别——按主要影响判定:过度暴露 = 权限过多(数据暴露给错误受众、匿名修改、PII 可被过多人员读取、用户专属数据设为 Global);暴露不足 = 权限不足(缺少必需的 CRUD 流程、面向用户的表没有权限、缺少角色/角色身份);正确性 = 结构性问题(引用了模型中不存在的表、角色链接悬空、范围错误/无效、缺少设计文档)。如果某个根本原因可能涉及两个类别,则选择其在现实中最严重的后果。将合并后问题的
dimension重新设为所选类别中的维度。 - **暴露不足维度的判定规则(按顺序匹配,优先采用第一个匹配项):**缺少角色身份,且涉及一个或多个可达能力 →
role-completeness;已有角色身份,但缺少表权限 →table-coverage;已有角色和表权限,但缺少 CRUD/append 位 →intent-coverage。选择这些维度时,绝不能依据描述中的强调程度。 - **计划/代码冲突的判定规则:**对于
reachable能力,以实现为准,并保留适用的 C6/C7/C15 major 问题;对于disabled能力,以计划为准,不提出暴露不足问题;对于ambiguous能力,仅提出 minor 的internal-consistency冲突问题。 - 不要合并不同表上的独立暴露问题。涉及表集互不重叠或受众/严重性不同的两个暴露/PII 问题属于不同的根本原因——都要保留(例如:学生/教师表上的匿名 PII 暴露(major),以及公共公告/设施的 Global 读取权限过宽(minor))。仅合并涉及相同表的问题。如有疑问,请分开保留。按定义,C16 和 C18 每个角色各占一个问题。
- 保留每个预检问题(步骤 2.5),作为独立问题,并保留其固定 id 和严重性。如果审计/评分标准中的问题与预检问题重复,则将其合并到预检问题中(保留预检问题的 id;严重性取两者中较高者)。
- 绝不臆造新问题;每个合并后的问题都必须能追溯到至少一个源问题。绝不为了减少数量而将 major 降级。
合并后的问题将用于评分及后续所有阶段。按(可能已重新分配的)维度重新归类。这个合并后的单一 major/minor 列表是下游所有环节的唯一问题集——它将成为报告的 FINDINGS_DATA(步骤 7.2)、概览中的 Major/Minor 数量,以及评分的输入。问题集唯一,统计口径统一,因此问题越多 ⇒ 分数越低。
5.3 计算分数(纯公式)
严格按照 附录 A 执行:根据各类别汇总的问题,为每个类别计算一个 1–5 分的分数(不计算总分);根据 major 问题数量确定判定结果;并统计所有合并后问题的数量 { major, minor, total }。
5.4 结论(正文)
撰写 2–3 句的评审结论:说明配置做得最好的地方、最主要的问题,以及最终结论。结论必须逐字写为:存在 零个重大问题时写 "Safe to go",存在 至少一个重大问题时写 "Needs revision"。次要问题不会影响结论。不要告知用户是否应该部署、发布或上线——结论只是总结发现,决定由用户作出。仅用正文。将其用作报告的 SUMMARY(步骤 7.2)。
如果缺少数据模型清单(SCORECARD_DATA 为 null),则跳过结论:撰写 2–3 句摘要,说明这是一次部分审计,并且不要使用上述任一结论短语。
步骤 6:根因归属(批量处理)
在一次批量处理中分析所有重大问题(完成合并后;跳过次要问题)。网站证据是共用的,因此在同一个推理步骤中对所有重大问题进行分类。对于每个问题,判断它是否纯粹由权限设计导致,或是否全部或部分由上游产物引起。务必保守判断——除非有明确的上游证据,否则默认归为 permissions(错误地归因于上游,比漏掉一个上游归因更糟)。
根因值:
permissions— 确实是权限设计问题;没有任何上游因素导致此问题。在权限或 Web 角色 YAML 中修复。data-model— 清单缺少表或列,或者使用了错误的发布者前缀。webapi-code— 网站代码(src/shared/services/*、src/types/*、页面)引用表或列的方式迫使权限采用特定形式,或依赖浏览器端规则。webapi-settings—Webapi-*-enabled/-fieldsYAML 迫使进行授权(或造成缺口)。mixed— 上游原因和权限方面的错误共同导致问题。
对于每个重大问题,确定其 rootCause 并撰写 explanation;它们将在步骤 7.2 中分别成为该问题的 rootCause 和 rootCauseReason。
explanation 会显示在重大问题旁边,因此请用一个通俗易懂的句子说明该标签的依据:对于 permissions,说明问题为何仅由权限配置导致;否则,指出具体的上游文件或设置,以及它造成的影响。
步骤 7:生成报告
7.1 确定输出位置
- 如果是在网站的上下文中工作(项目根目录中存在
powerpages.config.json):写入<PROJECT_ROOT>/docs/permissions-audit.html - 否则:写入系统临时目录
此文件夹即步骤 2.6 中定义的 <OUTPUT_DIR>。
7.2 准备数据
切勿手动生成 HTML,也不要自行读取或修改模板。 使用 render-audit-report.js 脚本,该脚本会机械地读取模板,并将占位符替换为你的数据。
编写一个临时 JSON 数据文件(例如 <OUTPUT_DIR>/audit-data.json),包含以下键:
{
"SITE_NAME": "The site name (from powerpages.config.json or folder name)",
"AUDIT_DESC": "Security audit of table permissions for Contoso Portal",
"SUMMARY": "2-3 sentence summary of the audit results",
"FINDINGS_DATA": [/* array of finding objects */],
"INVENTORY_DATA": [/* array of current permission objects */],
"SCORECARD_DATA": {/* grader overlay — see below; use null if scoring was skipped */}
}
SITE_NAME、AUDIT_DESC、SUMMARY、FINDINGS_DATA、INVENTORY_DATA 是渲染脚本会验证的必需键。增强版模板还会使用额外的键 SCORECARD_DATA——务必始终包含此键(对象或 null),以免遗留未替换的占位符。在 SCORECARD_DATA.reportPaths 中,列出 HTML 报告的绝对路径——概览标签页会显示此路径,供用户打开报告。
FINDINGS_DATA 格式——这是第 5.2 步的合并重大/次要问题列表(每个问题占一项;评分正是根据此列表计算的)。使用第 5 步中的规范问题 ID(例如 AH1)。对于重大问题,从该问题的第 6 步条目中复制 rootCause 和 explanation;次要问题则省略这两个字段:
{
"id": "AH1",
"severity": "major",
"title": "Staff email/phone readable by Anonymous",
"table": "contoso_staff",
"dimension": "anonymous-access-hygiene",
"scope": "Global",
"permission": "Staff - Public Read",
"reasoning": "Staff email/phone are in the $select and the Webapi field whitelist, and Staff-Public-Read grants the Anonymous role Global read — public internet can read staff PII.",
"fix": "Restrict staff read to a purpose-specific role assigned only to verified users, or drop contoso_email/contoso_phone from the whitelist and $select.",
"details": "Staff-Public-Read.tablepermission.yml; Webapi-contoso_staff-fields.sitesetting.yml; src/shared/services/staffService.ts:85",
"rootCause": "mixed",
"rootCauseReason": "The anonymous Global read grant is a permission mistake, and the Web API fields setting also whitelists contoso_email and contoso_phone."
}
id:第 5 步中的规范问题 ID——与第 6 步使用的 ID 相同。severity:major或minor——仅有这两个级别(绝不可使用 critical/warning/info/pass)。table:此问题涉及的表逻辑名称(一般性问题则为null)dimension:此问题归属的评分维度(通过附录 C 的映射关系)scope:适用时填写当前范围(数字代码或易读名称),否则为nullpermission:如果此问题涉及现有权限,则填写权限名称,否则为nullreasoning:详细说明问题所在——引用具体代码文件、代码行模式或 Dataverse 元数据fix:说明如何解决问题的可执行建议details:补充上下文,例如文件引用、列名称或关系详情rootCause:仅限重大问题——第 6 步中的根因(permissions、mixed、data-model、webapi-code或webapi-settings)rootCauseReason:仅限重大问题——第 6 步中的explanation,用一句话说明该根因的依据
INVENTORY_DATA 格式:
{
"name": "Product - Anonymous Read",
"table": "cra5b_product",
"scope": "Global",
"roles": ["Anonymous Users"],
"read": true,
"create": false,
"write": false,
"delete": false,
"append": true,
"appendto": false
}
SCORECARD_DATA 格式(来自第 5 步;报告只读取以下字段):
{
"categories": [
{ "name": "Over-Exposure (Security)", "score": 3.10 },
{ "name": "Under-Exposure (Usability & Coverage)", "score": 4.21 },
{ "name": "Correctness (Validity & Alignment)", "score": 3.48 }
],
"reportPaths": [
{ "label": "HTML report", "path": "<PROJECT_ROOT>/docs/permissions-audit.html" }
]
}
报告会自动显示这些数据。概览标签页首先展示审计摘要,然后是问题行(重大和次要问题数量——这是唯一的问题总数,也正是评分的依据)和结论部分(可以安全上线 / 需要修订,根据 FINDINGS_DATA 中的重大问题数量计算得出,同时显示各类别的 1–5 分评分条),以及报告路径。每个问题都属于重大或次要,报告中绝不存在 critical/warning/info/pass 级别。问题标签页会列出每个重大/次要问题及其证据和修复方法;每个重大问题还会显示一个根因徽标(仅权限、权限 + 上游或上游:数据模型 / 站点代码 / Web API 设置)以及一句话依据。
7.3 渲染 HTML 文件
运行渲染脚本(如果需要,该脚本会创建输出目录):
node "${PLUGIN_ROOT}/scripts/render-audit-report.js" --output "<OUTPUT_PATH>" --data "<DATA_JSON_PATH>"
渲染脚本不会覆盖现有文件。调用脚本前,检查默认输出路径(<PROJECT_ROOT>/docs/permissions-audit.html)是否已存在。如果已存在,请根据上下文选择一个新的描述性文件名,例如 permissions-audit-apr-2026.html、permissions-audit-post-migration.html。通过 --output 传入所选名称,并在 reportPaths 中使用相同路径。
7.4 验证报告
对渲染后的报告运行语义验证器:
node "${PLUGIN_ROOT}/skills/audit-permissions/scripts/validate-audit.js" \
--report "<OUTPUT_PATH>" \
--data "<DATA_JSON_PATH>" \
--auditRunId "<AUDIT_RUN_ID>"
如果本地运行初始化未返回运行 ID,请省略 --data 和 --auditRunId;仍必须运行报告验证。
验证成功之前,不要展示或复制报告。验证器会从报告中读取 FINDINGS_DATA、INVENTORY_DATA 和 SCORECARD_DATA,并检查问题 ID 是否唯一且前缀与维度匹配、严重和一般严重性、title/reasoning/fix 是否非空、每条重大发现是否具有有效的 rootCause 和 rootCauseReason(一般发现则不得包含这两项)、清单是否为数组、根据发现重新计算的三个类别分数是否正确、SUMMARY 中的结论是否与重大问题数量相符(部分审计中不得出现任一结论短语),以及是否存在未替换的占位符。提供运行 ID 和数据路径时,同一个确定性命令会重新验证源数据、推导完整的汇总指标,并记录成功完成状态和经过时长。
如果验证失败,请删除刚刚渲染的报告,修复数据文件中指出的问题,重新渲染到同一路径,然后再次运行验证器。不要为了修复验证错误而重复收集证据。
验证器会将 telemetryStatus 报告为 recorded_success、recorded_failure(报告有效,但其源数据无法核对,因此此运行已通过 failureStage: metrics_validation 记录)、disabled(此插件已硬性禁用遥测,因此没有记录任何内容)、failed 或 not_requested。遥测尽力而为:recorded_failure、disabled 或 failed 均不会使报告失效。不要使用精简后的负载重试,也不要在诊断输出中暴露报告数据。
只有在报告验证成功后,才能删除临时的 audit-data.json 和 audit-evidence.json 文件。遥测状态不影响清理。
7.5 在浏览器中打开
在用户的默认浏览器中打开实际的 HTML 输出路径。
第 8 步:展示发现并跟踪
8.1 记录技能使用情况
参考:
${PLUGIN_ROOT}/references/skill-tracking-reference.md
按照参考文档中的技能跟踪说明,记录此技能的使用情况。使用 --skillName "AuditPermissions"。
8.2 呈现摘要
🚦 关卡(计划 · audit-permissions:6.fix-offer): 提议自动修复主要/次要问题。报告已写入;此处拒绝只会保留 HTML 报告,不会修改 Dataverse / 文件系统。
触发条件: 第 8 阶段已汇总问题并保存报告。 询问原因: 工具可能会静默调用 table-permissions-architect Agent;默认接受会违背用户意愿,写入或修改权限 YAML。 取消后保留: 无更改——报告仍保存在原路径。不会写入 web-role / table-permission 文件。
向用户呈现摘要:
- 结论 — 当
SCORECARD_DATA非 null 时,显示 可以继续 或 需要修订,并列出三个类别的分数;否则说明这是部分审计,并省略这两个结论措辞和分数。 - 主要问题数量 — 这些问题需要立即处理
- 次要问题数量 — 属于可选改进 / 前瞻性建议
- 报告位置 — HTML 报告的保存位置
- 使用
AskUserQuestion询问用户:"Would you like me to fix any of these issues? I can create or update table permissions to resolve the major issues."
如果用户希望应用修复:
- 对于 403 / Web API 访问问题(缺少表权限、缺少 CRUD 标志、范围不正确、缺少 append/appendto、缺少
$expand读取权限覆盖——任何会导致 Power Pages Web API 返回 403 Forbidden 响应的问题):使用Agent工具启动 table-permissions-architect Agent。向其传递提示词,其中包含问题发现中提到的具体表、所需的 CRUD 标志、范围建议和关系详情。该 Agent 将分析站点、提出权限计划,并在用户批准后创建正确的表权限 YAML 文件。提示词示例:"Create table permissions for the following tables based on audit findings: <table1> needs Global scope with read:true; <table2> needs Parent scope under <parent_table> with read:true, create:true, append:true; <table3> needs appendto:true for lookups from <source_table>. The site project root is <PROJECT_ROOT>." - 对于非权限问题(例如未使用的权限、缩小范围的建议或其他次要问题):说明需要进行哪些手动更改,或建议运行
/integrate-webapi,以便 Web API 设置架构师处理站点设置级别的问题。
附录 A — 评分模型
评分是已汇总问题的纯函数——请原样复现。
类别
| 类别 | 维度 |
|---|---|
| 过度暴露(安全性) | privilege-calibration、anonymous-access-hygiene、security-posture |
| 暴露不足(可用性与覆盖范围) | intent-coverage、table-coverage、role-completeness |
| 正确性(有效性与一致性) | scope-correctness、data-model-alignment、internal-consistency |
根据问题计算分数的公式
对于一组问题,令 nMajor 和 nMinor 分别表示主要问题和次要问题的数量。那么:
load = 1.0 * nMajor + 0.25 * nMinor
penalty = tanh(0.2 * load) # tanh maps [0,∞) → [0,1); penalty(0)=0
score = 5 - 4 * penalty # 0 issues → 5.00 ; grows → 1.00
score = round(score * 100) / 100 # 2 decimals
常量:MAJOR_LOAD = 1.0、MINOR_LOAD = 0.25、PENALTY_STEEPNESS = 0.2。
- 类别分数 =
scoreFromIssues(该类别各维度的问题合并去重后所得的问题集)。类别是评分单位——不要对各维度的分数取平均值。 - 不存在总分。 三个类别的分数并列报告,绝不合并为一个数字。
参考值(陡峭度为 0.2):0 个问题 → 5.00;1 个主要问题 → 4.21;2 个主要问题 → 3.48;3 个主要问题 → 2.85;1 个次要问题 → 4.80;第一个主要问题的影响始终最大(收益递减)。
最终结论
结论由严重程度决定,而非任何评分:
| 条件 | 结论 | 含义 |
|---|---|---|
| 0 个重大问题 | 可以安全进行 | 未发现重大问题。 |
| ≥ 1 个重大问题 | 需要修改 | 发现了一个或多个重大问题;请检查并处理。 |
轻微问题不会改变结论——它们仍会被记录,也会降低所属类别的评分,但如何处理由用户判断。请在步骤 5.4 和报告中逐字写出结论短语。
结论仅供参考。它总结了审计发现的问题;这不是批准,也不是发布门槛,是否采取行动由人工审查者决定。切勿指示用户部署或不部署。
附录 B — 9 个维度
使用各维度的定义来判断问题所属类别。输出顺序固定如下。
1. 意图覆盖 — IC · 覆盖不足 — 请求中每项与授权相关的能力,是否至少对应一个(角色 × 表 × 操作)授权?可用性规则:如果请求暗示需要管理或编写记录,但没有任何角色对相关表拥有任何创建/写入/删除权限(所有地方都仅可读取),则属于覆盖失败 → 一个重大问题。
2. 权限校准 — PC · 权限过宽 — 对于每个(角色、表、操作),每项 CRUD 权限是否恰到好处——既不过度授权,也不授权不足?
3. 范围正确性 — SC · 正确性 — 范围(全局/联系人/帐户/父级/自身)是否符合数据所有权模型?按用户划分 → 联系人;公共公告 → 全局;通过父记录访问的子记录 → 父级;用户自己的联系人记录 → 自身。
4. 角色完整性 — RC · 覆盖不足 — 请求暗示的所有用户类型是否都已作为 Web 角色纳入?是否存在多余角色?匿名和已验证身份用户是否设置正确?
5. 表覆盖(权限) — TC · 覆盖不足 — 每个面向用户的表是否至少有一项权限?系统表/非面向用户的表是否已正确排除?
6. 匿名访问规范 — AH · 权限过宽 — 仅在请求暗示内容公开时授予匿名用户最低限度的只读权限;除非有正当理由,否则不得授予匿名用户写入/创建/删除权限。
7. 数据模型对齐 — DM · 正确性 — 权限中引用的每个表是否都对应实际数据模型中的表?是否存在虚构或命名错误的表?
8. 内部一致性 — IS · 正确性 — YAML 是否有效?权限中每个 Web 角色 GUID 是否都能解析?permissions-plan.html 是否与 YAML 一致?
9. 安全态势 — SP · 权限过宽 — 整体是否遵循最小权限原则?敏感表(联系人、系统表)上是否不存在意外的宽泛授权?共享记录上是否不存在可避免的删除/写入权限?如果存在 Web API 字段白名单,请检查它们是否未宽泛暴露敏感 PII 列。
附录 C — 严重程度政策与审计↔评分器对应关系
严重级别策略(按顺序应用规则;以第一个匹配项为准)
只有两个严重级别:major(确实存在的问题——会导致站点故障或数据泄露,或存在确实不理想、应当修复的配置)和 minor(外观问题/面向未来/锦上添花)。评分器中没有「info」级别(审计中的 info 发现项归为 minor)。
默认立场——安全优先/最小权限。 只有当角色的实际使用场景确实需要时,才应授予访问权限,并将范围限制得尽可能小。访问或修改超出此需求的记录属于权限授予过宽 → major,即使该角色本身「已获授权」也是如此。
- 已发生还是假设情况。 Major = 问题当前确实存在(某个流程已中断,或数据正被不应看到它的受众访问)。Minor = 只在假设的未来变更下才会产生影响,或仅涉及外观。Minor 的典型措辞:「如果以后……」、「可能」、「应当重新审视」。
- 按需知悉/范围过宽。 Major = 某个角色可以读取/修改超出其需求的记录——或者是未获授权的受众(匿名用户,或不属于数据预期受众的角色)能够访问,或者是获授权的角色对按所有者/关系划分的数据(个人记录、PII、成绩、名单、监护人联系方式)获得了全局范围,因此能够访问所有记录,而非仅访问其所属子集。PII(电子邮件、电话号码、监护人联系方式、地址)尤其严重。Minor = 仅对确实属于公开/共享参考的数据(公告、设施、公开目录)存在范围过宽。处理共享工作队列的员工角色遵循 C16;与其父表访问范围相匹配的子表遵循 C17。
- 修改与读取。 Major = 不应拥有相应权限的角色获得了非预期的修改权限(写入/创建/删除;匿名用户拥有修改权限始终属于 major)。范围过宽的读取属于 minor,除非符合规则(2)。缺少必要的修改权限(管理/内容创作站点在任何地方都没有创建/写入/删除权限)会导致流程中断 → 按规则(1)归为 major。
参考案例。 常见问题目录(C1–C18,第 5.1 步)是主要参考标准集——目录涵盖的案例必须完全按照目录中的说明标注。另外还有两个常见案例:
- 员工/教师/管理员对含 PII 的内部表拥有全局范围的读取权限 → major(规则 2)。
- 某个角色对超出需求的按所有者划分的运营数据拥有全局范围(例如教师可以读取/编辑所有学生的成绩,而不仅限于自己班级的成绩)→ major(规则 2)。
审计发现 → 评分维度对应关系
将每项第 4 步的发现映射到一个维度,然后应用上文的严重性策略,设为 major/minor。(审计级别为 critical 时几乎总是 → major;审计级别为 info 时 → minor;审计级别为 warning 时 → 应用该策略。)
| 审计检查 / 发现 | 评分维度 | 典型评分严重性 |
|---|---|---|
| A. 被引用的表缺少权限 | 意图覆盖度(+ 表覆盖度) | major |
| A. 未使用的权限(代码中未引用) | 权限校准(C18,每个角色计为一项问题) | minor |
| B. 权限没有 Web 角色 | 角色完整性 | major;对于具有有效父级链的 Parent 范围子项,不计问题 |
| C. 全局范围且具有写入/删除权限 | 权限校准 / 安全态势 | major,但员工共享工作队列(C16)和子表权限对齐(C17)除外 |
| C. 范围可以进一步缩小 | 范围正确性 | 涉及每个所有者/PII 时为 major,否则为 minor |
| D. 缺少读取权限 | 意图覆盖度 | major |
| E. 缺少创建权限 · F. 缺少写入权限 · G. 缺少删除权限 | 意图覆盖度 | major |
| E/F/G. 已启用但未使用 | 权限校准(C18,每个角色计为一项问题) | minor |
| F. 上传文件但禁用了写入权限 | 意图覆盖度 | major |
| F. 有写入权限但没有读取权限 | 权限校准 | major |
| H. 缺少 append · H2. 缺少 appendto | 权限校准 / 意图覆盖度 | major |
| H. 已启用 append,但不需要 | 权限校准(C18,每个角色计为一项问题) | minor |
| I. 父级链损坏 | 内部一致性 | major |
J. $expand 表缺少读取权限 | 意图覆盖度 | major |
| 匿名用户可读取 PII | 匿名访问规范 / 安全态势 | major |
架构验证器的 error / warning / info | 内部一致性(或具体维度) | error→major,warning→策略判定,info→minor |
关键约束
- 只读分析:此技能只读取现有配置和代码。除非用户明确要求修复问题(步骤 8),否则不会修改任何文件。评分和报告生成均不会修改数据。
- 绝不臆造评分:始终根据汇总后的问题数量,按照 附录 A 推导评分。同样的问题 ⇒ 同样的评分。
- 预检问题是事实依据:在汇总和评分过程中保留步骤 2.5 中的问题(id、维度);绝不丢弃或降低其严重程度——合并重复问题时,只能提高严重程度。
- 每个根本原因对应一个问题:不要将单个根本原因拆分为多个问题;也不要过度合并彼此独立的问题。
- 最终标签只分配一次:步骤 4 生成事实性候选项;只有步骤 5 才分配最终严重程度、维度和问题 id。
- 可达性优先于文字描述:已路由或可调用的门户行为优先于与之矛盾的计划文本;明确的运行时/构建排除优先于未启用的源代码;歧义属于轻微正确性问题,不应默认判定为曝光不足。
- 客户端筛选器不等于授权:根据内容敏感度对可绕过限制的行分类——机密/敏感信息泄露属于重大问题;非敏感的草稿/工作流信息泄露属于轻微问题。
- 最小权限/安全优先决定维度严重程度(附录 C)和重复问题目录(步骤 5.1)。员工角色无权访问所有记录——只能访问与其相关的记录。
- 管理员无需表权限,除非可访问的网站代码会执行仅限管理员的增删改查;否则绝不将缺少此类权限标记为问题;参见步骤 5.1 中的 ADMINISTRATORS EXCEPTION。
- 确定性 API 调用:始终使用 Node.js 脚本(
query-table-lookups.js、query-table-relationships.js)查询 Dataverse API——绝不使用内联 PowerShellInvoke-RestMethod调用。 - 分析期间不得提问:自主收集所有数据、运行检查、评分并呈现发现。只有在最后才询问用户是否要修复问题。
- 安全:绝不记录或显示身份验证令牌。脚本会通过
getAuthToken()在内部获取令牌。 - 优雅降级:如果 Dataverse API 脚本失败(退出代码 1),跳过依赖 API 的检查(H/H2 append/appendto 验证、I 父级链完整性),并在报告中注明跳过了哪些检查。如果缺少数据模型清单,仍须输出审计发现;将
SCORECARD_DATA设为null,不得捏造评分——此时报告会显示部分审计提示,而非结论和评分,且SUMMARY会说明未得出结论(步骤 5.4)。 - 不要臆造输入中不存在的文件或表格。