
Audit Permissions
- 121 installs
- 572 repo stars
- Updated July 28, 2026
- microsoft/power-platform-skills
>-.
About
>-. > **Plugin check**: Run `node "${PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding. The audit-permissions skill documents workflows, constraints, and examples from SKILL.md for agent-assisted execution.
- > **Plugin check**: Run `node "${PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user
- **Verify Site Deployment** — Check that `.powerpages-site` folder and table permissions exist
- **Gather Configuration** — Read all web roles, table permissions, and site code
- **Run Local Schema Validation** — Use the shared validator to detect invalid permission/site-setting YAML before deeper
- 4. **Analyze & Discover** — Query Dataverse for relationships and lookup columns using deterministic scripts
Audit Permissions by the numbers
- 121 all-time installs (skills.sh)
- Ranked #508 of 1,453 DevOps & CI/CD skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
audit-permissions capabilities & compatibility
- Capabilities
- > **plugin check**: run `node "${plugin_root}/sc · **verify site deployment** — check that `.powerp · **gather configuration** — read all web roles, t · **run local schema validation** — use the shared
- Use cases
- documentation
What audit-permissions says it does
>-
npx skills add https://github.com/microsoft/power-platform-skills --skill audit-permissionsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 121 |
|---|---|
| repo stars | ★ 572 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 28, 2026 |
| Repository | microsoft/power-platform-skills ↗ |
How do I apply audit-permissions using the workflow in its SKILL.md?
>-
Who is it for?
Developers following the audit-permissions skill for the tasks it documents.
Skip if: Tasks outside the audit-permissions scope described in SKILL.md.
When should I use this skill?
User mentions audit-permissions or related triggers from the skill description.
What you get
Working audit-permissions setup aligned with the documented patterns and constraints.
Files
Plugin check: Run node "${PLUGIN_ROOT}/scripts/check-version.js" — if it outputs a message, show it to the user before proceeding.Audit Permissions
Audit existing table permissions on a Power Pages code site. Analyze permissions against the site code and Dataverse metadata, then generate a visual HTML audit report with findings, reasoning, and suggested fixes.
Workflow
1. Verify Site Deployment — Check that .powerpages-site folder and table permissions exist 2. Gather Configuration — Read all web roles, table permissions, and site code 3. Run Local Schema Validation — Use the shared validator to detect invalid permission/site-setting YAML before deeper analysis 4. Analyze & Discover — Query Dataverse for relationships and lookup columns using deterministic scripts 5. Run Audit Checks — Compare permissions against code usage and best practices 6. Generate Report — Create the HTML audit report and display in browser 7. Present Findings & Track — Summarize findings, record skill usage, and ask user if they want to fix issues
Important: Do NOT ask the user questions during analysis. Autonomously gather all data, then present findings.
Task Tracking
At the start of Step 1, create all tasks upfront using TaskCreate. Mark each task in_progress when starting and completed when done.
| Task subject | activeForm | Description |
|---|---|---|
| Verify site deployment | Verifying site deployment | Check .powerpages-site folder and table permissions exist |
| Gather configuration | Gathering configuration | Read web roles, table permissions, and site code |
| Run local schema validation | Validating local permissions schema | Run shared validator against existing table permission and site setting YAML |
| Discover relationships | Discovering relationships | Query Dataverse for lookup columns and relationships |
| Run audit checks | Running audit checks | Create per-table tasks and run checklist (A–K) for each table, then cross-validate |
| Generate audit report | Generating audit report | Create HTML report and display in browser |
| Present findings | Presenting findings | Summarize results, record usage, and offer to fix issues |
Note: The "Run audit checks" phase creates additional per-table tasks dynamically in Step 4.2. These per-table tasks track the systematic A–K checklist for each table independently.
---
Step 1: Verify Site Deployment
Use Glob to find:
**/powerpages.config.json— identifies the project root**/.powerpages-site/table-permissions/*.tablepermission.yml— existing permissions
If no .powerpages-site folder exists, stop and tell the user to deploy first using /deploy-site. If no table permissions exist, note this as a critical finding (the site may have no data access configured) and continue the audit — there may still be code references that need permissions.
---
Step 2: Gather Configuration
2.1 Read Web Roles
Read all files matching **/.powerpages-site/web-roles/*.yml. Extract id, name, anonymoususersrole, authenticatedusersrole from each.
2.2 Read Table Permissions
Read all files matching **/.powerpages-site/table-permissions/*.tablepermission.yml. For each permission, extract:
entityname(permission name)entitylogicalname(table)scope(numeric code)read,create,write,delete,append,appendto(boolean flags)adx_entitypermission_webrole(array of web role UUIDs)contactrelationship,accountrelationship(if Contact/Account scope)parententitypermission,parentrelationship(if parent scope)
2.3 Analyze Site Code
Search the site source code for:
- Web API calls (
/_api/) - Lookup bindings (
@odata.bind) - File uploads (
uploadFileColumn,uploadFile,upload*Photo,upload*Image) $expandusage ($expand,buildExpandClause,ExpandOption)
Also check for .datamodel-manifest.json in the project root for the authoritative table list.
Build a map of: which tables are referenced in code, which CRUD operations are performed on each, which lookup relationships are used, and which related tables are fetched via $expand (these need read permissions too).
2.4 Run Shared Schema Validator
Run the shared validator against the existing site:
node "${PLUGIN_ROOT}/scripts/validate-permissions-schema.js" --projectRoot "<PROJECT_ROOT>"Parse the JSON output and carry the findings into the audit. Treat:
errorfindings as criticalwarningfindings as warninginfofindings as info
These findings should be included in the final audit report even if the later code/Dataverse analysis also finds additional issues.
After Step 3.1 determines the environment URL, if this audit is running locally with Dataverse access available, rerun the shared validator with live relationship verification enabled and merge any additional findings:
node "${PLUGIN_ROOT}/scripts/validate-permissions-schema.js" --projectRoot "<PROJECT_ROOT>" --validate-dataverse-relationships --envUrl "<envUrl>"Use this Dataverse-backed relationship validation only for local runs. Do not require it in CI or other offline contexts.
---
Step 3: Analyze & Discover (Dataverse API)
Use deterministic Node.js scripts for all Dataverse API calls. These scripts handle auth token acquisition, HTTP requests, and JSON parsing consistently.
3.1 Get Environment URL
pac env whoExtract the Environment URL (e.g., https://org12345.crm.dynamics.com) and use it as <envUrl> in subsequent script calls.
3.2 Query Lookup Columns
For each table that has permissions with create or write enabled, use the lookup query script:
node "${PLUGIN_ROOT}/skills/audit-permissions/scripts/query-table-lookups.js" --envUrl "<envUrl>" --table "<table_logical_name>"The script returns a JSON array of { logicalName, targets } for each lookup column. Capture this output for the maps described below.
After querying all tables with create or write permissions, build two maps from the combined results:
1. Source map (table → lookup columns): For each queried table, record which lookup columns it has and their targets. Used in Section H2 to check appendto on the source table. 2. Reverse target map (target table → list of source tables): For each target table found in any lookup's targets array, record which source table(s) reference it. Used in Section H to check append on the target table.
Example: querying order_item returns [{ logicalName: "cr4fc_orderid", targets: ["cr4fc_order"] }]
- Source map:
order_item → [{ column: "cr4fc_orderid", targets: ["cr4fc_order"] }] - Reverse target map:
cr4fc_order → [{ sourceTable: "order_item", column: "cr4fc_orderid" }]
Both maps are used in Sections H and H2:
- The source table (with the lookup) needs
appendto: true— it links TO other records (checked via the source map) - Each target table in
targetsneedsappend: true— other records link TO it (checked via the reverse target map)
3.3 Query Relationships
For tables with parent-scope permissions, verify the relationship names using the relationship query script:
node "${PLUGIN_ROOT}/skills/audit-permissions/scripts/query-table-relationships.js" --envUrl "<envUrl>" --table "<parent_table>"The script returns a JSON array of { schemaName, referencedEntity, referencingEntity, referencingAttribute }. Use schemaName to validate the parentrelationship value in parent-scope permissions.
Error Handling
If any script exits with code 1, skip the API-dependent checks and note which checks were skipped in the report. Do NOT stop the entire audit for auth errors. Use the data model manifest and code analysis as fallback.
---
Step 4: Run Audit Checks
Use per-table task tracking to systematically run every audit check. Each check produces a finding with severity, title, reasoning, and a suggested fix. Findings can be critical, warning, info, or pass.
4.1 Build Audit Inventory
First, build a combined list of all tables to audit from two sources:
1. Tables referenced in code (from Step 2.3) — these may or may not have permissions 2. Tables with existing permissions (from Step 2.2) — these may or may not be referenced in code
The union of these two sets is the complete audit scope. Each table will be audited from both directions: "does the code need a permission that doesn't exist?" and "does the permission match what the code actually does?"
4.2 Create Per-Table Audit Tasks
For each table in the audit inventory, create a task:
TaskCreate:
subject: "Audit <table_logical_name>"
activeForm: "Auditing <table_display_name> permissions"
description: "Run all audit checks for <table_logical_name>"Also create a summary task:
TaskCreate:
subject: "Compile audit findings"
activeForm: "Compiling audit findings"
description: "Combine all per-table findings into the final report"Use TaskList at any point to review progress and see which tables still need auditing.
4.3 Per-Table Audit Checklist
For each table, mark its task in_progress and run through the following checks in order. For every finding, note the specific evidence (file path, permission name, code pattern) that supports it. Skip checks that don't apply to this table.
A. Permission Existence
Does this table have a table permission?
- If the table is referenced in code but has no permission → finding:
- Severity:
critical - Title:
Missing permission for <table> - Reasoning: Which code files reference this table and what operations they perform
- Fix: Create a permission with the appropriate scope and CRUD flags
- If a permission exists but the table is not referenced in code → finding:
- Severity:
info - Title:
Unused permission for <table> - Reasoning: The table is not referenced in any source code — the permission may be unnecessary
- Fix: Review whether this permission is still needed
- If both exist →
pass, proceed to remaining checks
B. Web Role Association
Does the permission have web role(s) assigned?
- Check
adx_entitypermission_webrole— if empty or missing → finding: - Severity:
warning - Title:
Permission <name> has no web role association - Reasoning: A permission without a web role has no effect — no users will receive this access
- Fix: Associate with the appropriate web role
- If roles are assigned →
pass
C. Scope Appropriateness
Is the scope the least-privileged option that fits?
- Search the service code for scope-relevant patterns: contact-scoped filters (
getCurrentContactId,_contactid_value,contactid) and account-scoped filters (_accountid_value,parentcustomerid) - If Global scope (
756150000) withwriteordeleteenabled → finding: - Severity:
warning - Title:
Global scope with write/delete on <table> - Reasoning: Any user with this role can modify/delete any record in this table
- Fix: Narrow to Contact or Account scope, or remove write/delete if not needed
- If Global scope with only
read→pass(acceptable for public reference data) - If code uses contact-scoped filters but permission uses Global → finding:
- Severity:
warning - Title:
Scope could be narrower for <table> - Reasoning: Code filters by current contact but permission grants Global access
- Fix: Narrow to Contact scope
- Otherwise →
pass
D. Read Permission
Is read correctly set?
- Search the service code for GET/list/get patterns for this table: API calls to
/_api/<entity_set>, list/get functions (list<TableName>,get<TableName>) - If code reads this table but
read: false→ finding: - Severity:
critical - Title:
Missing read permission for <table> - Reasoning: Code reads from this table but permission does not grant read access
- Fix: Enable
read: true - If
read: trueand code reads →pass
E. Create Permission
Is create correctly set?
- Search the service code for POST/create patterns: POST method usage (
method: 'POST'), create functions (create<TableName>) - If code creates records but
create: false→ finding: - Severity:
critical - Title:
Missing create permission for <table> - Reasoning: Code creates records in this table but permission does not grant create access
- Fix: Enable
create: true - If
create: truebut no create patterns in code → finding: - Severity:
info - Title:
Create enabled but not used for <table> - Reasoning: No create operations found in code — permission may be overly permissive
- Fix: Consider disabling
createif not needed - If matched →
pass
F. Write Permission
Is write correctly set?
- Search the service code for PATCH/update/upload patterns: PATCH method usage (
method: 'PATCH'), update functions (update<TableName>), file upload patterns (uploadFileColumn,uploadFile,upload*Photo,upload*Image,upload*File) - If code updates records but
write: false→ finding: - Severity:
critical - Title:
Missing write permission for <table> - Reasoning: Code updates records (or uploads files) in this table but permission does not grant write access
- Fix: Enable
write: true - If file upload patterns found but
write: false→ finding: - Severity:
warning - Title:
File upload detected but write is disabled on <table> - Reasoning: File uploads use PATCH which requires write permission
- Fix: Enable
write: true - If
write: truebutread: false→ finding: - Severity:
warning - Title:
Write enabled without read on <table> - Reasoning: Users can modify records they cannot see, which is unusual and likely unintended
- Fix: Enable
read: true - If
write: truebut no write patterns in code → finding: - Severity:
info - Title:
Write enabled but not used for <table> - Reasoning: No update operations found in code — permission may be overly permissive
- Fix: Consider disabling
writeif not needed - If matched →
pass
G. Delete Permission
Is delete correctly set?
- Search the service code for DELETE patterns: DELETE method usage (
method: 'DELETE'), delete functions (delete<TableName>) - If code deletes records but
delete: false→ finding: - Severity:
critical - Title:
Missing delete permission for <table> - Reasoning: Code deletes records in this table but permission does not grant delete access
- Fix: Enable
delete: true - If
delete: truebut no delete patterns in code → finding: - Severity:
info - Title:
Delete enabled but not used for <table> - Reasoning: No delete operations found in code — permission may be overly permissive
- Fix: Consider disabling
deleteif not needed - If matched →
pass
H. Append (target table check)
Does this table need append: true? Append is required on the target table — the table that other records link TO via lookup columns.
- Check the reverse target map from Step 3.2: is this table referenced as a lookup target by any other table that has
createorwritepermissions? - Also search the service code for
@odata.bindreferences to this table's entity set (e.g.,/<entity_set>() - If this table appears in the reverse target map (i.e., another table with create/write has a lookup to this table), but
append: false→ finding: - Severity:
critical - Title:
Missing append on <table> - Reasoning: Table
<source_table>has lookup column<column>targeting this table and sets it during create/write. The target table needs append permission so records can be linked to it. Users will see "You don't have permission to associate or disassociate" - Fix: Enable
append: true - If
append: trueand justified →pass - If
append: truebut this table does NOT appear in the reverse target map and no code references it as a lookup target → finding: - Severity:
info - Title:
Append enabled but not needed on <table> - Reasoning: No other table with create/write has a lookup to this table — append may be unnecessary
- Fix: Consider disabling
appendif not needed
H2. AppendTo (source table check)
Does this table need appendto: true? AppendTo is required on the source table — the table that has lookup columns linking TO other records.
- Check the source map from Step 3.2: does this table have lookup columns?
- Also search the service code for
@odata.bindpatterns in create/update calls for this table - If this table has lookup columns (in source map or code) AND has
createorwriteenabled, butappendto: false→ finding: - Severity:
critical - Title:
Missing appendto on <table> - Reasoning: This table sets lookup column
<column>targeting<target_table>during create/write, which requires appendto permission. Users will see "You don't have permission to associate or disassociate" - Fix: Enable
appendto: true - If
appendto: trueand justified →pass
I. Parent Chain Integrity
If the permission has Parent scope (756150003):
- Verify
parententitypermissionreferences a valid permission ID that exists - Verify
parentrelationshipis a valid Dataverse relationship (if API available, using Step 3.3 results) - If broken → finding:
- Severity:
critical - Title:
Broken parent chain for <permission> - Reasoning: The parent permission reference is invalid — this permission will not grant any access
- Fix: Correct the parent permission ID and/or relationship name
- If valid →
pass
J. $expand Related Table Coverage
Is this table fetched via $expand on another table's query?
- Check the
$expandanalysis from Step 2.3 (search site source code for$expand,buildExpandClause,ExpandOption) - If this table is expanded from another table but has no table permission with
read: truefor the same web role → finding: - Severity:
critical - Title:
Missing read permission for expanded table <table> - Reasoning: This table is fetched via
$expandon<parent_table>in<service_file>, but has no read permission. Power Pages enforces table permissions on every entity in the query. - Fix: Create a table permission with
read: truefor the same web role. For collection-valued expansions (one-to-many), use Parent scope with the relationship name. For single-valued expansions (lookups to reference data), use Global scope with read-only access. - If properly covered →
pass
K. Record Findings & Complete
After all checks, mark the table's task as completed via TaskUpdate.
4.4 Cross-Table Validation
After all per-table audits are complete, run these cross-table checks:
1. Append/AppendTo consistency: Using the source map and reverse target map from Step 3.2, verify: (a) every source table (with lookups and create/write) has appendto: true, (b) every target table in the reverse map has append: true, (c) no table has appendto: true without lookup columns in the source map, (d) no table has append: true without being in the reverse target map 2. $expand coverage: For every $expand usage, verify the expanded table has read: true 3. Parent chain completeness: For every Parent scope permission, verify the parent permission exists and is valid 4. Web role consistency: If two related tables (e.g., parent and child) are accessed by the same feature, verify they share the same web role assignment
Use TaskList to review all completed audits, then mark the "Compile audit findings" task as in_progress and proceed to Step 5.
---
Step 5: Generate Report
5.1 Determine Output Location
- If working in context of a website (project root with
powerpages.config.jsonexists): write to<PROJECT_ROOT>/docs/permissions-audit.html - Otherwise: write to the system temp directory
5.2 Prepare Data
Do NOT generate HTML manually or read/modify the template yourself. Use the render-audit-report.js script which mechanically reads the template and replaces placeholder tokens with your data.
Write a temporary JSON data file (e.g., <OUTPUT_DIR>/audit-data.json) with these keys:
{
"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 */]
}FINDINGS_DATA format:
{
"id": "f1",
"severity": "critical",
"title": "Missing permission for cra5b_product",
"table": "cra5b_product",
"scope": null,
"permission": null,
"reasoning": "The table cra5b_product is referenced in src/services/productService.ts with GET requests to /_api/cra5b_products, but no table permission exists for this table.",
"fix": "Create a table permission with Global scope and read-only access for the Anonymous Users role.",
"details": "Referenced in: src/services/productService.ts (line 23), src/components/ProductList.tsx (line 45)"
}severity: One ofcritical,warning,info,passtable: The table logical name this finding relates to (ornullfor general findings)scope: The current scope if applicable (numeric code or friendly name), ornullpermission: The permission name if this finding is about an existing permission, ornullreasoning: Detailed explanation of why this is an issue — reference specific code files, line patterns, or Dataverse metadatafix: Actionable suggestion for how to resolve the issue (ornullforpassfindings)details: Additional context like file references, column names, or relationship details
INVENTORY_DATA format:
{
"name": "Product - Anonymous Read",
"table": "cra5b_product",
"scope": "Global",
"roles": ["Anonymous Users"],
"read": true,
"create": false,
"write": false,
"delete": false,
"append": true,
"appendto": false
}5.3 Render the HTML File
Run the render script (it creates the output directory if needed):
node "${PLUGIN_ROOT}/scripts/render-audit-report.js" --output "<OUTPUT_PATH>" --data "<DATA_JSON_PATH>"The render script refuses to overwrite existing files. Before calling it, check if the default output path (<PROJECT_ROOT>/docs/permissions-audit.html) already exists. If it does, choose a new descriptive filename based on context — e.g., permissions-audit-apr-2026.html, permissions-audit-post-migration.html. Pass the chosen name via --output.
Delete the temporary data JSON file after the script succeeds.
5.4 Open in Browser
Open the actual output path in the user's default browser.
---
Step 6: Present Findings & Track
6.1 Record Skill Usage
Reference: ${PLUGIN_ROOT}/references/skill-tracking-reference.mdFollow the skill tracking instructions in the reference to record this skill's usage. Use --skillName "AuditPermissions".
6.2 Present Summary
<!-- gate: audit-permissions:6.fix-offer | category=plan | cancel-leaves=nothing -->
🚦 Gate (plan · audit-permissions:6.fix-offer): Offer to apply auto-fixes for critical/warning findings. The audit report has already been written; declining here just leaves the HTML report in place — no Dataverse / filesystem mutation.
>
Trigger: Phase 6 has tallied findings and the HTML report is saved.
Why we ask: Tooling could silently invoke the table-permissions-architect agent — accept-by-default would write or mutate permission YAML against the user's intent.
Cancel leaves: Nothing — the audit report stays at its saved path. No web-role / table-permission files written.
Present a summary to the user:
1. Critical findings count — these need immediate attention 2. Warning findings count — should be addressed 3. Report location — where the HTML file was saved 4. Ask the user using AskUserQuestion: "Would you like me to fix any of these issues? I can create or update table permissions to resolve the critical and warning findings."
If the user wants fixes applied:
- For 403 / Web API access issues (missing table permissions, missing CRUD flags, incorrect scope, missing append/appendto, missing
$expandread coverage — any finding that would result in a 403 Forbidden response from the Power Pages Web API): Spawn the table-permissions-architect agent using theAgenttool. Pass it a prompt that includes the specific tables, the required CRUD flags, scope recommendations, and relationship details from the audit findings. The agent will analyze the site, propose a permissions plan, and create the correct table permission YAML files after user approval. Example prompt:"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>." - For non-permission issues (e.g., unused permissions, scope narrowing suggestions, informational findings): explain what manual changes are needed or suggest running
/integrate-webapiso the Web API settings architect can address site-setting-level issues.
---
Critical Constraints
- Read-only analysis: This skill only reads existing configuration and code. It does NOT modify any files unless the user explicitly asks to fix issues.
- Deterministic API calls: Always use the Node.js scripts (
query-table-lookups.js,query-table-relationships.js) for Dataverse API queries — never use inline PowerShellInvoke-RestMethodcalls. - No questions during analysis: Autonomously gather all data, run checks, and present findings. Only ask the user at the end about fixing issues.
- Security: Never log or display auth tokens. The scripts handle token acquisition internally via
getAuthToken(). - Graceful degradation: If Dataverse API scripts fail (exit code 1), skip API-dependent checks (H/H2 append/appendto validation, I parent chain integrity) and note in the report which checks were skipped.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<title>Permissions Audit — __SITE_NAME__</title>
<style>
:root {
--bg:#faf9f8; --surface:#ffffff; --surface2:#f3f2f1; --surface3:#edebe9;
--border:#e1dfdd; --border-light:#c8c6c4;
--text:#323130; --text-dim:#605e5c; --text-bright:#201f1e;
--accent:#0078d4; --accent-bg:#0078d40a; --accent-border:#0078d425;
--critical:#d13438; --critical-bg:#d134380a; --critical-border:#d1343825;
--warning:#ca5010; --warning-bg:#ca50100a; --warning-border:#ca501025;
--pass:#107c10; --pass-bg:#107c100a; --pass-border:#107c1025;
--info:#0078d4; --info-bg:#0078d40a; --info-border:#0078d425;
--purple:#8764b8; --purple-bg:#8764b80a; --purple-border:#8764b825;
--mono:'Cascadia Code','Consolas',monospace;
--sans:'Segoe UI','Segoe UI Web (West European)',-apple-system,system-ui,sans-serif;
--radius:8px; --radius-sm:4px;
--shadow-4:0 1.6px 3.6px 0 rgba(0,0,0,0.132),0 0.3px 0.9px 0 rgba(0,0,0,0.108);
--shadow-8:0 3.2px 7.2px 0 rgba(0,0,0,0.132),0 0.6px 1.8px 0 rgba(0,0,0,0.108);
}
*{margin:0;padding:0;box-sizing:border-box;}
html{scroll-behavior:smooth;}
body{font-family:var(--sans);background:var(--bg);color:var(--text);font-size:14px;line-height:1.6;}
/* Top Bar */
.topbar{z-index:100;background:var(--surface);box-shadow:var(--shadow-4);padding:14px 28px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;}
.topbar-left{display:flex;align-items:center;gap:14px;}
.logo{width:36px;height:36px;object-fit:contain;display:block;flex-shrink:0;}
.topbar-title{font-size:16px;font-weight:700;color:var(--text-bright);}
.topbar-sub{font-size:11px;color:var(--text-dim);margin-top:1px;}
/* Layout */
.layout{display:flex;min-height:calc(100vh - 61px);}
/* Sidebar */
.sidebar{width:200px;background:var(--surface);border-right:1px solid var(--border);padding:20px 0;flex-shrink:0;}
.nav-btn{display:flex;align-items:center;gap:10px;width:100%;padding:11px 22px;background:none;border:none;border-left:2px solid transparent;color:var(--text-dim);font-size:13px;font-weight:500;cursor:pointer;font-family:var(--sans);text-align:left;transition:all 0.15s;position:relative;}
.nav-btn:hover{color:var(--text);background:var(--surface2);}
.nav-btn.active{color:var(--accent);font-weight:600;border-left-color:var(--accent);background:var(--accent-bg);}
.nav-btn .nav-icon{font-size:15px;opacity:0.5;width:18px;text-align:center;}
.nav-btn.active .nav-icon{opacity:0.9;}
.nav-badge{position:absolute;right:14px;font-size:10px;font-family:var(--mono);padding:1px 6px;border-radius:8px;font-weight:700;}
/* Content */
.content{flex:1;padding:32px 40px;max-width:960px;}
.section{display:none;}
.section.active{display:block;animation:fadeIn 0.3s ease;}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:translateY(0);}}
h2{font-size:21px;font-weight:800;color:var(--text-bright);letter-spacing:-0.3px;margin-bottom:5px;}
.section-desc{font-size:13px;color:var(--text-dim);margin-bottom:20px;}
h3{font-size:15px;font-weight:700;color:var(--text-bright);margin-top:24px;margin-bottom:12px;}
/* Cards */
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px 20px;margin-bottom:10px;transition:box-shadow 0.2s;}
.card:hover{box-shadow:var(--shadow-4);}
/* Stats Grid */
.stats-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:24px;}
.stat-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px 16px;text-align:center;position:relative;overflow:hidden;box-shadow:var(--shadow-4);}
.stat-card::after{content:'';position:absolute;top:0;left:50%;transform:translateX(-50%);width:40px;height:2px;border-radius:0 0 2px 2px;}
.stat-num{font-size:28px;font-weight:800;font-family:var(--mono);line-height:1;}
.stat-label{font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px;margin-top:6px;}
/* Severity badges */
.severity{font-size:9px;font-weight:700;padding:3px 8px;border-radius:3px;font-family:var(--mono);display:inline-block;text-transform:uppercase;letter-spacing:0.5px;}
.severity-critical{color:var(--critical);background:var(--critical-bg);border:1px solid var(--critical-border);}
.severity-warning{color:var(--warning);background:var(--warning-bg);border:1px solid var(--warning-border);}
.severity-info{color:var(--info);background:var(--info-bg);border:1px solid var(--info-border);}
.severity-pass{color:var(--pass);background:var(--pass-bg);border:1px solid var(--pass-border);}
/* Finding cards */
.finding-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:12px;transition:box-shadow 0.2s;overflow:hidden;}
.finding-card:hover{box-shadow:var(--shadow-4);}
.finding-header{display:flex;align-items:flex-start;gap:12px;padding:16px 20px;cursor:pointer;-webkit-user-select:none;user-select:none;}
.finding-header:hover{background:var(--surface2);}
.finding-title{font-size:14px;font-weight:600;color:var(--text-bright);flex:1;}
.finding-table{font-size:11px;color:var(--accent);background:var(--accent-bg);padding:1px 6px;border-radius:3px;border:1px solid var(--accent-border);font-family:var(--mono);}
.finding-chevron{color:var(--text-dim);font-size:12px;transition:transform 0.2s;flex-shrink:0;margin-top:3px;}
.finding-card.expanded .finding-chevron{transform:rotate(90deg);}
.finding-body{display:none;padding:0 20px 16px;border-top:1px solid var(--border);}
.finding-card.expanded .finding-body{display:block;animation:fadeIn 0.2s ease;}
/* Field label */
.field-label{font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px;}
/* Reasoning & fix blocks */
.reasoning-block{background:var(--surface2);border-left:2px solid var(--accent);border-radius:var(--radius-sm);padding:10px 14px;margin-top:10px;font-size:12px;color:var(--text);line-height:1.7;}
.fix-block{background:var(--pass-bg);border-left:2px solid var(--pass);border-radius:var(--radius-sm);padding:10px 14px;margin-top:8px;font-size:12px;color:var(--text);line-height:1.7;}
.fix-block strong{color:var(--pass);}
/* Scope Tags */
.scope-tag{font-size:10px;font-weight:700;padding:2px 8px;border-radius:3px;font-family:var(--mono);display:inline-block;text-align:center;min-width:58px;}
.scope-Global{color:var(--critical);background:var(--critical-bg);border:1px solid var(--critical-border);}
.scope-Contact{color:var(--pass);background:var(--pass-bg);border:1px solid var(--pass-border);}
.scope-Account{color:var(--accent);background:var(--accent-bg);border:1px solid var(--accent-border);}
.scope-Parent{color:var(--purple);background:var(--purple-bg);border:1px solid var(--purple-border);}
.scope-Self{color:var(--text-dim);background:var(--surface2);border:1px solid var(--border);}
/* Privilege Chips */
.priv{font-size:9px;font-weight:700;padding:2px 5px;border-radius:3px;font-family:var(--mono);}
.priv-on{color:var(--pass);background:var(--pass-bg);}
.priv-off{color:var(--text-dim);background:var(--surface2);opacity:0.5;}
/* Inventory table */
.inv-table{width:100%;border-collapse:collapse;font-size:13px;margin:10px 0;}
.inv-table th{text-align:left;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.8px;color:var(--text-dim);padding:10px 12px;border-bottom:2px solid var(--border);}
.inv-table td{padding:10px 12px;border-bottom:1px solid var(--border);vertical-align:top;}
.inv-table tr:last-child td{border-bottom:none;}
.inv-table tr:hover td{background:var(--surface2);}
/* Filter buttons */
.filter-bar{display:flex;gap:6px;margin-bottom:16px;flex-wrap:wrap;}
.filter-btn{padding:5px 12px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);color:var(--text-dim);font-size:12px;font-weight:600;cursor:pointer;font-family:var(--sans);transition:all 0.15s;}
.filter-btn:hover{border-color:var(--border-light);color:var(--text);}
.filter-btn.active{background:var(--accent-bg);border-color:var(--accent-border);color:var(--accent);}
.filter-count{font-family:var(--mono);font-size:10px;margin-left:4px;opacity:0.7;}
/* Responsive */
@media(max-width:768px){
.sidebar{display:none;}
.content{padding:20px 16px;}
.stats-grid{grid-template-columns:repeat(2,1fr);}
}
</style>
</head>
<body>
<div id="mainApp">
<div class="topbar">
<div class="topbar-left">
<img class="logo" src="./power-pages-icon.png" alt="Power Pages" />
<div>
<div class="topbar-title">Permissions Audit Report</div>
<div class="topbar-sub">__SITE_NAME__</div>
</div>
</div>
</div>
<div class="layout">
<div class="sidebar">
<button class="nav-btn active" data-tab="overview"><span class="nav-icon">◉</span> Overview</button>
<button class="nav-btn" data-tab="findings"><span class="nav-icon">⚠</span> Findings
<span class="nav-badge" id="navFindingsCount" style="background:var(--critical-bg);color:var(--critical);"></span>
</button>
<button class="nav-btn" data-tab="inventory"><span class="nav-icon">▦</span> Inventory</button>
</div>
<div class="content">
<!-- OVERVIEW -->
<div class="section active" id="tab-overview">
<h2>Audit Overview</h2>
<p class="section-desc">__AUDIT_DESC__</p>
<div class="stats-grid">
<div class="stat-card" style="--accent-color:var(--critical)"><div class="stat-num" style="color:var(--critical)" id="statCritical">0</div><div class="stat-label">Critical</div></div>
<div class="stat-card"><div class="stat-num" style="color:var(--warning)" id="statWarning">0</div><div class="stat-label">Warning</div></div>
<div class="stat-card"><div class="stat-num" style="color:var(--info)" id="statInfo">0</div><div class="stat-label">Info</div></div>
<div class="stat-card"><div class="stat-num" style="color:var(--pass)" id="statPassed">0</div><div class="stat-label">Passed</div></div>
</div>
<h3>Audit Summary</h3>
<div class="card" id="summaryBox" style="line-height:1.75;">__SUMMARY__</div>
</div>
<!-- FINDINGS -->
<div class="section" id="tab-findings">
<h2>Findings</h2>
<p class="section-desc" id="findingsDesc"></p>
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;">
<div class="filter-bar" id="filterBar" style="margin-bottom:0;"></div>
<button class="filter-btn" id="expandAllBtn" style="white-space:nowrap;">Expand All</button>
</div>
<div id="findingsContainer" style="margin-top:16px;"></div>
</div>
<!-- INVENTORY -->
<div class="section" id="tab-inventory">
<h2>Permissions Inventory</h2>
<p class="section-desc" id="invDesc"></p>
<h3>Legend</h3>
<div class="card" style="margin-bottom:16px;">
<div style="display:flex;gap:32px;flex-wrap:wrap;">
<div>
<div class="field-label" style="margin-bottom:6px;">Scope</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;">
<span class="scope-tag scope-Global">Global</span>
<span class="scope-tag scope-Contact">Contact</span>
<span class="scope-tag scope-Account">Account</span>
<span class="scope-tag scope-Parent">Parent</span>
<span class="scope-tag scope-Self">Self</span>
</div>
<div style="font-size:11px;color:var(--text-dim);margin-top:6px;line-height:1.6;">
<strong>Global</strong> — all records |
<strong>Contact</strong> — user's contact |
<strong>Account</strong> — user's account |
<strong>Parent</strong> — via parent table |
<strong>Self</strong> — own record only
</div>
</div>
<div>
<div class="field-label" style="margin-bottom:6px;">Privileges</div>
<div style="display:flex;flex-direction:column;gap:4px;font-size:12px;">
<div style="display:flex;align-items:center;gap:6px;"><span class="priv priv-on">R</span><strong style="color:var(--text-bright);">Read</strong><span style="color:var(--text-dim);">— Retrieve and view records</span></div>
<div style="display:flex;align-items:center;gap:6px;"><span class="priv priv-on">C</span><strong style="color:var(--text-bright);">Create</strong><span style="color:var(--text-dim);">— Add new records</span></div>
<div style="display:flex;align-items:center;gap:6px;"><span class="priv priv-on">W</span><strong style="color:var(--text-bright);">Write</strong><span style="color:var(--text-dim);">— Update existing records (includes file uploads via PATCH)</span></div>
<div style="display:flex;align-items:center;gap:6px;"><span class="priv priv-on">D</span><strong style="color:var(--text-bright);">Delete</strong><span style="color:var(--text-dim);">— Remove records</span></div>
<div style="display:flex;align-items:center;gap:6px;"><span class="priv priv-on">A</span><strong style="color:var(--text-bright);">Append</strong><span style="color:var(--text-dim);">— Attach this record to another via a lookup</span></div>
<div style="display:flex;align-items:center;gap:6px;"><span class="priv priv-on">AT</span><strong style="color:var(--text-bright);">AppendTo</strong><span style="color:var(--text-dim);">— Allow other records to reference this one via a lookup</span></div>
</div>
<div style="font-size:11px;color:var(--text-dim);margin-top:8px;">
<span class="priv priv-on" style="font-size:8px;">R</span> enabled
<span class="priv priv-off" style="font-size:8px;">R</span> disabled
</div>
</div>
</div>
</div>
<div id="invContainer"></div>
</div>
</div>
</div>
</div>
<script>
// Data populated by the audit-permissions skill
const SITE_NAME = "__SITE_NAME__";
const FINDINGS = __FINDINGS_DATA__;
// Each finding: { id, severity (critical|warning|info|pass), title, table, scope, permission, reasoning, fix, details }
const INVENTORY = __INVENTORY_DATA__;
// Each item: { name, table, scope, roles, read, create, write, delete, append, appendto }
// Tab navigation
document.querySelectorAll('.nav-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
});
});
// Severity config
function esc(s) { if (!s) return ''; return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
function escAttr(s) { return String(s || '').replace(/[^a-zA-Z0-9_.-]/g, ''); }
const sevConfig = {
critical: { color: 'var(--critical)', label: 'CRITICAL', icon: '\u2716' },
warning: { color: 'var(--warning)', label: 'WARNING', icon: '\u26A0' },
info: { color: 'var(--info)', label: 'INFO', icon: '\u2139' },
pass: { color: 'var(--pass)', label: 'PASSED', icon: '\u2714' }
};
const scopeNames = { 756150000:'Global', 756150001:'Contact', 756150002:'Account', 756150003:'Parent', 756150004:'Self' };
function scopeName(s) { return scopeNames[s] || s; }
// Stats
const criticalCount = FINDINGS.filter(f => f.severity === 'critical').length;
const warningCount = FINDINGS.filter(f => f.severity === 'warning').length;
const infoCount = FINDINGS.filter(f => f.severity === 'info').length;
const passCount = FINDINGS.filter(f => f.severity === 'pass').length;
const issueCount = criticalCount + warningCount + infoCount;
document.getElementById('statCritical').textContent = criticalCount;
document.getElementById('statWarning').textContent = warningCount;
document.getElementById('statInfo').textContent = infoCount;
document.getElementById('statPassed').textContent = passCount;
document.getElementById('navFindingsCount').textContent = issueCount > 0 ? issueCount : '';
document.getElementById('findingsDesc').textContent =
FINDINGS.length + ' check' + (FINDINGS.length !== 1 ? 's' : '') +
' \u2014 ' + criticalCount + ' critical, ' + warningCount + ' warning, ' + infoCount + ' info, ' + passCount + ' passed.';
document.getElementById('invDesc').textContent =
INVENTORY.length + ' table permission' + (INVENTORY.length !== 1 ? 's' : '') + ' currently configured.';
// Filter bar
let activeFilter = 'all';
function renderFilterBar() {
const c = document.getElementById('filterBar');
const counts = { all: FINDINGS.length, critical: criticalCount, warning: warningCount, info: infoCount, pass: passCount };
const labels = { all: 'All', critical: 'Critical', warning: 'Warning', info: 'Info', pass: 'Passed' };
let html = '';
Object.entries(labels).forEach(([key, label]) => {
if (counts[key] === 0 && key !== 'all') return;
html += '<button class="filter-btn ' + (activeFilter === key ? 'active' : '') + '" data-filter="' + escAttr(key) + '">' + esc(label) + '<span class="filter-count">' + counts[key] + '</span></button>';
});
c.innerHTML = html;
}
function setFilter(f) {
activeFilter = f;
renderFilterBar();
renderFindings();
}
// Render findings
function renderFindings() {
const c = document.getElementById('findingsContainer');
const filtered = activeFilter === 'all' ? FINDINGS : FINDINGS.filter(f => f.severity === activeFilter);
const order = { critical: 0, warning: 1, info: 2, pass: 3 };
const sorted = [...filtered].sort((a, b) => (order[a.severity] ?? 99) - (order[b.severity] ?? 99));
let html = '';
sorted.forEach(f => {
const sev = sevConfig[f.severity] || sevConfig.info;
const fid = escAttr(f.id);
const fsev = escAttr(f.severity);
html += '<div class="finding-card" id="finding-' + fid + '">' +
'<div class="finding-header">' +
'<span class="severity severity-' + fsev + '">' + esc(sev.label) + '</span>' +
'<span class="finding-title">' + esc(f.title) + '</span>' +
(f.table ? '<span class="finding-table">' + esc(f.table) + '</span>' : '') +
'<span class="finding-chevron">▶</span>' +
'</div>' +
'<div class="finding-body">' +
(f.permission ? '<div style="margin-top:10px;font-size:12px;"><span class="field-label">Permission:</span> <span style="font-weight:600;color:var(--text-bright)">' + esc(f.permission) + '</span></div>' : '') +
(f.scope ? '<div style="margin-top:6px;font-size:12px;"><span class="field-label">Scope:</span> <span class="scope-tag scope-' + escAttr(scopeName(f.scope)) + '" style="font-size:10px;padding:1px 6px;min-width:auto;">' + esc(scopeName(f.scope)) + '</span></div>' : '') +
(f.details ? '<div style="margin-top:10px;font-size:12px;color:var(--text);line-height:1.7;">' + esc(f.details) + '</div>' : '') +
(f.reasoning ? '<div style="margin-top:10px;"><div class="field-label">Reasoning</div><div class="reasoning-block">' + esc(f.reasoning) + '</div></div>' : '') +
(f.fix ? '<div style="margin-top:8px;"><div class="field-label">Suggested Fix</div><div class="fix-block"><strong>Fix:</strong> ' + esc(f.fix) + '</div></div>' : '') +
'</div>' +
'</div>';
});
if (sorted.length === 0) {
html = '<div style="text-align:center;padding:40px;color:var(--text-dim);font-size:14px;">No findings for this filter.</div>';
}
c.innerHTML = html;
}
// Expand/Collapse All
let allExpanded = false;
function toggleExpandAll() {
allExpanded = !allExpanded;
document.querySelectorAll('.finding-card').forEach(c => {
if (allExpanded) c.classList.add('expanded');
else c.classList.remove('expanded');
});
document.getElementById('expandAllBtn').textContent = allExpanded ? 'Collapse All' : 'Expand All';
}
// Render inventory
function renderInventory() {
const c = document.getElementById('invContainer');
if (INVENTORY.length === 0) {
c.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text-dim);font-size:14px;">No table permissions configured.</div>';
return;
}
const privs = ['Read','Create','Write','Delete','Append','AppendTo'];
let html = '<table class="inv-table"><thead><tr><th>Permission Name</th><th>Table</th><th>Scope</th><th>Roles</th><th>Privileges</th></tr></thead><tbody>';
INVENTORY.forEach(p => {
const flags = { Read: p.read, Create: p.create, Write: p.write, Delete: p.delete, Append: p.append, AppendTo: p.appendto };
html += '<tr>' +
'<td style="font-weight:600;color:var(--text-bright)">' + esc(p.name) + '</td>' +
'<td><code style="font-size:11px;color:var(--accent);background:var(--accent-bg);padding:1px 5px;border-radius:3px;border:1px solid var(--accent-border);">' + esc(p.table) + '</code></td>' +
'<td><span class="scope-tag scope-' + escAttr(scopeName(p.scope)) + '" style="font-size:9px;padding:1px 6px;min-width:auto;">' + esc(scopeName(p.scope)) + '</span></td>' +
'<td style="font-size:12px;">' + (Array.isArray(p.roles) ? esc(p.roles.join(', ')) : esc(p.roles)) + '</td>' +
'<td style="display:flex;gap:3px;flex-wrap:wrap;">' + privs.map(function(pr) { return '<span class="priv ' + (flags[pr] ? 'priv-on' : 'priv-off') + '">' + pr[0] + (pr === 'AppendTo' ? 'T' : '') + '</span>'; }).join('') + '</td>' +
'</tr>';
});
html += '</tbody></table>';
c.innerHTML = html;
}
document.getElementById('expandAllBtn').addEventListener('click', toggleExpandAll);
// Event delegation — no inline onclick handlers
document.addEventListener('click', function(e) {
var btn = e.target.closest('.filter-btn[data-filter]');
if (btn) { setFilter(btn.dataset.filter); return; }
var hdr = e.target.closest('.finding-header');
if (hdr) { hdr.closest('.finding-card').classList.toggle('expanded'); }
});
// Init
renderFilterBar();
renderFindings();
renderInventory();
</script>
<footer style="position:fixed;bottom:0;left:0;right:0;text-align:center;padding:10px;font-size:12px;color:#605e5c;border-top:1px solid #c8c6c4;background:#ffffff;z-index:100;">AI-generated content may be incorrect</footer>
</body>
</html>
#!/usr/bin/env node
// Queries Dataverse for lookup columns on a given table.
// Returns JSON array of { logicalName, targets } for each lookup column.
//
// Usage:
// node query-table-lookups.js --envUrl <url> --table <logical_name>
//
// Output (stdout): JSON array
// [{ "logicalName": "cr4fc_categoryid", "targets": ["cr4fc_category"] }]
//
// Exit codes:
// 0 = success (JSON on stdout)
// 1 = error (message on stderr)
const { getAuthToken, makeRequest } = require('../../../scripts/lib/validation-helpers');
const args = process.argv.slice(2);
function getArg(name) {
const idx = args.indexOf('--' + name);
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null;
}
const envUrl = getArg('envUrl');
const table = getArg('table');
if (!envUrl || !table) {
process.stderr.write('Usage: node query-table-lookups.js --envUrl <url> --table <logical_name>\n');
process.exit(1);
}
(async () => {
const token = getAuthToken(envUrl);
if (!token) {
process.stderr.write('Failed to get auth token. Run: az login --allow-no-subscriptions\n');
process.exit(1);
}
try {
const result = await makeRequest({
url: `${envUrl}/api/data/v9.2/EntityDefinitions(LogicalName='${table}')/Attributes/Microsoft.Dynamics.CRM.LookupAttributeMetadata?$select=LogicalName,Targets`,
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/json',
},
timeout: 15000,
});
if (result.error || result.statusCode !== 200) {
process.stderr.write(`API error (${result.statusCode}): ${result.error || result.body}\n`);
process.exit(1);
}
const parsed = JSON.parse(result.body);
const lookups = (parsed.value || []).map(attr => ({
logicalName: attr.LogicalName,
targets: attr.Targets || [],
}));
process.stdout.write(JSON.stringify(lookups, null, 2) + '\n');
} catch (err) {
process.stderr.write(`Request failed: ${err.message}\n`);
process.exit(1);
}
})();
#!/usr/bin/env node
// Thin CLI wrapper over scripts/lib/query-table-relationships.js.
// Queries Dataverse for one-to-many relationships on a given table.
// Returns JSON array of { schemaName, referencedEntity, referencingEntity, referencingAttribute }.
//
// Usage:
// node query-table-relationships.js --envUrl <url> --table <logical_name>
//
// Output (stdout): JSON array (OneToMany relationships only — the audit-permissions
// relationship-scope validation consumes schemaName + referencedEntity).
//
// Exit codes:
// 0 = success (JSON on stdout)
// 1 = error (message on stderr)
const { getAuthToken } = require('../../../scripts/lib/validation-helpers');
const { fetchTableRelationships } = require('../../../scripts/lib/query-table-relationships');
const args = process.argv.slice(2);
function getArg(name) {
const idx = args.indexOf('--' + name);
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null;
}
const envUrl = getArg('envUrl');
const table = getArg('table');
if (!envUrl || !table) {
process.stderr.write('Usage: node query-table-relationships.js --envUrl <url> --table <logical_name>\n');
process.exit(1);
}
(async () => {
const token = getAuthToken(envUrl);
if (!token) {
process.stderr.write('Failed to get auth token. Run: az login --allow-no-subscriptions\n');
process.exit(1);
}
try {
// OneToMany errors propagate here (preserves the original exit-1-on-API-error
// behavior); ManyToMany is best-effort inside the lib and unused by this CLI.
const { oneToMany } = await fetchTableRelationships(envUrl, table, token);
process.stdout.write(JSON.stringify(oneToMany, null, 2) + '\n');
} catch (err) {
process.stderr.write(`Request failed: ${err.message}\n`);
process.exit(1);
}
})();
#!/usr/bin/env node
// Validates that the permissions audit report was generated.
// Runs as a Stop hook to verify the skill produced output.
const fs = require('fs');
const path = require('path');
const { approve, block, runValidation, findPath, findProjectRoot } = require('../../../scripts/lib/validation-helpers');
runValidation((cwd) => {
const projectRoot = findProjectRoot(cwd);
if (!projectRoot) approve(); // Not a Power Pages project — not an audit session
// Check if audit report was generated in docs/
const docsReport = path.join(projectRoot, 'docs', 'permissions-audit.html');
if (fs.existsSync(docsReport)) {
const content = fs.readFileSync(docsReport, 'utf8');
if (content.includes('__FINDINGS_DATA__') || content.includes('__INVENTORY_DATA__')) {
block('Audit report has unreplaced placeholders — data was not populated.');
}
approve();
}
// Check temp directory as fallback
const tempDir = process.env.TEMP || process.env.TMP || '/tmp';
const tempReport = path.join(tempDir, 'permissions-audit.html');
if (fs.existsSync(tempReport)) {
approve();
}
// No report found — this may not be an audit session, so don't block
approve();
});
Related skills
FAQ
What does audit-permissions do?
>-
When should I use audit-permissions?
Invoke when >-.
Is audit-permissions safe to install?
Review the Security Audits panel on this page before installing in production.