Skip to content

Fortigi/FortigiGraph

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

387 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FortigiGraph

Unlock the insights hidden in your Entra ID Governance that the Azure Portal doesn't show you.

FortigiGraph syncs Microsoft Graph data to Azure SQL with temporal versioning, enabling powerful governance insights, access analysis, and identity auditing that simply aren't possible through the Entra ID portal alone.


Getting Started

FortigiGraph provides a guided setup wizard that creates everything you need: Azure resources, App Registration with the right permissions, SQL Server, and a config file that drives all operations.

Prerequisites

  • PowerShell 7+ (recommended) or PowerShell 5.1
  • Azure subscription with Contributor access
  • Az PowerShell module: Install-Module Az -Scope CurrentUser

Step 1: Install the Module

# From PowerShell Gallery
Install-Module -Name FortigiGraph -Scope CurrentUser

# Or clone and import locally
git clone https://github.com/Fortigi/FortigiGraph.git
cd FortigiGraph
Import-Module .\FortigiGraph.psd1

Step 2: Run the Setup Wizard

New-FGConfig -Path .\Config\mycompany.json

The wizard walks you through:

  • Azure Login - Logs you in and selects your subscription
  • Resource Group - Select an existing one or create a new one
  • SQL Server - Creates an Azure SQL Server and database (or selects existing)
  • Automation Account - Creates an Azure Automation Account for scheduled syncs
  • App Registration - Creates the app with the right Graph API permissions:
    • User.Read.All, Group.Read.All, GroupMember.Read.All
    • Directory.Read.All, EntitlementManagement.Read.All
    • AccessReview.Read.All, AuditLog.Read.All
  • Sync Settings - Choose which data to sync (users, groups, memberships, access packages, etc.)

At the end, it saves everything to a config file and shows you the next steps.

Step 3: Authenticate and Connect

# Authenticate to Microsoft Graph
Get-FGAccessToken -ConfigFile '.\Config\mycompany.json'

# Connect to Azure SQL Server (updates firewall automatically)
Connect-FGSQLServer -ConfigFile '.\Config\mycompany.json'

Step 4: Run Your First Sync

Start-FGSync -ConfigFile '.\Config\mycompany.json'

This syncs all enabled data types in parallel:

  • Users, Groups, Group Memberships (direct, eligible, owners)
  • Access Package Catalogs, Packages, Assignments, Policies, Requests, Reviews
  • Creates performance indexes and analytical SQL views automatically

Step 5: Set Up Scheduled Syncs (Optional)

New-FGAzureAutomationAccount -ConfigFile '.\Config\mycompany.json'

This creates an Azure Automation Account with:

  • Encrypted variables for all credentials
  • Runbooks for each sync type
  • Daily schedules (optional)
  • SQL firewall rule for Azure services

Step 6: Deploy the Role Mining UI

New-FGUI -ConfigFile '.\Config\mycompany.json'

This deploys a web application to Azure App Service that visualizes your synced data as an interactive permission matrix. It is the recommended way to explore and analyze your Entra ID governance data.

See the Role Mining UI section below for full details on the UI features.

Verify Your Data

# Check what tables were created
Get-FGSQLTable

# Query some data
Invoke-FGSQLQuery -Query "SELECT COUNT(*) AS UserCount FROM GraphUsers"
Invoke-FGSQLQuery -Query "SELECT TOP 5 displayName, userPrincipalName FROM GraphUsers"

# Check sync log
Invoke-FGSQLQuery -Query "SELECT * FROM GraphSyncLog ORDER BY StartTime DESC"

Create a Read-Only User for Power BI (Optional)

New-FGSQLReadOnlyUser -ConfigFile '.\Config\mycompany.json'

Role Mining UI

FortigiGraph includes a web-based Role Mining UI that visualizes your permission data as an interactive matrix, making it easy to discover role patterns and governance gaps. This is the primary way most users will interact with their synced data.

Deployment

# Deploy the UI (creates Azure App Service + App Registration + deploys code)
New-FGUI -ConfigFile '.\Config\mycompany.json'

# Deploy without authentication (for demos/development)
New-FGUI -ConfigFile '.\Config\mycompany.json' -NoAuth

# Redeploy after code changes (code-only, no resource creation)
Update-FGUI -ConfigFile '.\Config\mycompany.json'

# Remove the UI (stops billing)
Remove-FGUI -ConfigFile '.\Config\mycompany.json'

Architecture

Layer Technology Purpose
Backend Node.js + Express REST API querying FortigiGraph SQL views
Frontend React + Vite + Tailwind CSS + TanStack Table v8 Interactive SPA
Authentication Entra ID (MSAL) Supports v1 + v2 JWT token formats; -NoAuth for demos
Deployment Azure App Service (Linux, Node 20, P0v3) Oryx build-on-deploy
Data Sources vw_UserPermissionAssignments, vw_UserPermissionAssignmentViaAccessPackage, GraphUsers, GraphGroups SQL views + tables created by Start-FGSync

Pages

The UI has five pages accessible via tab navigation:

Matrix View (default)

The core visualization — an interactive user-group permission matrix.

  • Rows = groups, Columns = users. Each cell shows the membership types (Direct, Indirect, Eligible, Owner) as colored badges
  • Staircase Sort: Default row order groups rows by their leftmost access package, creating a visual staircase pattern. Unmanaged groups appear at the bottom
  • Access Package Coloring: Managed cells are colored by their governing access package (15-color palette). Multi-AP cells show a count badge
  • Access Package Columns: SOLL columns sorted first by category name, then by assignment count within each category; uncategorized access packages appear at the end. Category boundaries are marked with thicker borders and a colored indicator stripe.
  • IST/SOLL Toggle: Filter to show all assignments, only unmanaged (IST), or only managed (SOLL)
  • Server-Side User Limit: Slider (default 25) limits data at the SQL level for large environments
  • Drag-and-Drop: Reorder rows to group related permissions together
  • Excel Export: Full matrix export with AP-colored cells, rich-text badges, multi-AP notes, and AP columns next to users (matching the on-screen layout)
  • Share Link: Copy a URL that preserves all active filters, user limit, and managed toggle

Filtering is split into two sections:

Section Fields Applied
User Filters All user attributes (department, job title, company, city, etc.) + User Tag Server-side (full dataset)
Group Filters Group name, membership type, Group Tag Client-side (current page)

Filters use a pill-based UI: click "+ Add filter" → select field → select value. Active filters appear as removable pills with inline value switching.

Column header filters: The Type and Tags columns have filter dropdowns in the column header. The Tags filter includes a "(Blank)" option to show only groups without any tags assigned.

Users Page

Browse and manage all synced users with pagination.

  • Tag Management: Create colored tags, assign/remove tags from selected users, bulk-tag all matching a filter
  • Filtering: Same pill-based FilterBar with all user attribute columns + User Tag
  • Text Search: Search by display name or UPN
  • Selection: Checkbox selection with bulk tag operations

Groups Page

Browse and manage all synced groups with pagination.

  • Tag Management: Create colored tags, assign/remove tags from selected groups, bulk-tag by filter
  • Filtering: Pill-based FilterBar with all group attribute columns + Group Tag
  • Text Search: Search by group name or description
  • Selection: Checkbox selection with bulk tag operations

Access Packages Page

Browse all synced access packages with their catalog, assignment count, and category.

  • Category Management: Create colored categories, assign a category to selected access packages, or set it directly via an inline dropdown per row
  • Filtering: Filter by category (click a category pill) or show only uncategorized packages
  • Text Search: Search by access package name or catalog name
  • Selection: Checkbox selection with bulk category operations

Unlike tags (which allow multiple per entity), each access package can have only one category assigned. Categories drive the column ordering in the Matrix view.

Sync Log

View the last 50 sync operations from GraphSyncLog, showing timestamps, entity types, row counts, and durations.

Tagging System

Tags are user-defined labels (e.g. "VIP", "Contractors", "Finance Groups") that can be assigned to users or groups. They serve two purposes:

  1. Organization: Visually label entities in the Users/Groups tables
  2. Filtering: Use as filter criteria on any page (Users, Groups, or Matrix)

Tags are stored in the GraphTags and GraphTagAssignments SQL tables (auto-created on first use). Clicking a tag pill on the Users/Groups page adds it as a filter; it also appears as a "User Tag" or "Group Tag" option in the standard filter bar.

Category System

Categories are user-defined labels for access packages (e.g. "Identity", "Office 365", "Security"). Unlike tags, each access package can only have one category — this enforces clean grouping. Categories serve two purposes:

  1. Organization: Label access packages on the Access Packages page
  2. Matrix Column Ordering: AP columns in the Matrix view are sorted by category name first, then by assignment count within each category. Uncategorized APs appear at the end.

Categories are stored in the GraphCategories and GraphCategoryAssignments SQL tables (auto-created on first use). The GraphCategoryAssignments table has a primary key on accessPackageId, enforcing the single-category constraint.

UI API Reference

All endpoints require Authorization: Bearer <JWT> unless auth is disabled (-NoAuth). The backend runs on port 3001 and serves the React SPA for non-API routes.

Unauthenticated Endpoints

Method Path Description
GET /api/health Health check. Returns { status: "ok", mode: "sql"|"mock" }
GET /api/auth-config Auth configuration for MSAL. Returns { enabled, clientId?, tenantId? }

Matrix / Permissions

Method Path Description
GET /api/user-columns Column discovery for Matrix filters. Returns filterable columns from GraphUsers with up to 500 distinct values per column. Includes virtual __userTag and __groupTag columns if tags exist.
GET /api/permissions Main matrix data. Returns permission assignments with all user attributes, access package mappings, and total user count.
GET /api/access-package-groups Access package → group mapping with role names and assignment counts.
GET /api/sync-log Recent sync log entries from GraphSyncLog.

GET /api/permissions query parameters:

Parameter Type Description
userLimit int Limit to top N users by assignment count. 0 = all users.
filters JSON string Server-side filters: {"department":"HR","__userTag":"VIP"}

Response:

{
  "data": [
    {
      "groupId": "uuid",
      "groupDisplayName": "SG-Finance-Base",
      "memberId": "uuid",
      "memberDisplayName": "Jane Doe",
      "membershipType": "Direct",
      "department": "Finance",
      "jobTitle": "Analyst",
      "managedByAccessPackage": true
    }
  ],
  "totalUsers": 156,
  "managedByPackages": [
    { "memberId": "uuid", "groupId": "uuid", "accessPackageIds": ["ap-001"] }
  ]
}

GET /api/sync-log query parameters:

Parameter Type Default Description
limit int 20 Number of entries (max 100)

Users Page

Method Path Description
GET /api/user-columns-page Column discovery for Users page filters. Same format as /api/user-columns but scoped to the Users page. Includes __userTag virtual column.
GET /api/users Paginated user list with tags.

GET /api/users query parameters:

Parameter Type Default Description
search string Search displayName or UPN (LIKE)
tagId int Filter by tag ID (legacy, still supported)
limit int 100 Page size (max 500)
offset int 0 Pagination offset
filters JSON string Attribute filters: {"department":"HR","__userTag":"VIP"}

Response:

{
  "data": [
    {
      "id": "uuid",
      "displayName": "Jane Doe",
      "userPrincipalName": "jane@contoso.com",
      "department": "Finance",
      "jobTitle": "Analyst",
      "companyName": "Contoso",
      "accountEnabled": true,
      "tags": [{ "id": 1, "name": "VIP", "color": "#3b82f6" }]
    }
  ],
  "total": 1234
}

Groups Page

Method Path Description
GET /api/group-columns Column discovery for Groups page filters. Includes __groupTag virtual column.
GET /api/groups Paginated group list with tags.

GET /api/groups query parameters:

Parameter Type Default Description
search string Search displayName or description (LIKE)
tagId int Filter by tag ID (legacy, still supported)
limit int 100 Page size (max 500)
offset int 0 Pagination offset
filters JSON string Attribute filters: {"groupTypeCalculated":"Security","__groupTag":"Critical"}

Response:

{
  "data": [
    {
      "id": "uuid",
      "displayName": "SG-Finance-Base",
      "groupTypeCalculated": "Security",
      "description": "Base access for Finance",
      "tags": [{ "id": 2, "name": "Critical", "color": "#ef4444" }]
    }
  ],
  "total": 567
}

Tag Management

Method Path Description
GET /api/tags?entityType=user|group List tags (optionally filtered by entity type). Returns name, color, assignment count.
POST /api/tags Create a tag. Body: { name, color?, entityType }. Unique per (name, entityType).
PATCH /api/tags/:id Update tag name and/or color. Body: { name?, color? }
DELETE /api/tags/:id Delete tag and all its assignments (cascade).
POST /api/tags/:id/assign Assign tag to specific entities. Body: { entityIds: ["uuid", ...] }
POST /api/tags/:id/unassign Remove tag from specific entities. Body: { entityIds: ["uuid", ...] }
POST /api/tags/:id/assign-by-filter Bulk-assign tag to all entities matching a search/filter. Body: { entityType, search?, filters? }

Category Management

Method Path Description
GET /api/categories List all categories with assignment count.
POST /api/categories Create a category. Body: { name, color? }. Name must be unique.
PATCH /api/categories/:id Update category name and/or color. Body: { name?, color? }
DELETE /api/categories/:id Delete category and all its assignments (cascade).
POST /api/categories/:id/assign Assign category to an access package (replaces any existing category). Body: { accessPackageId }
POST /api/categories/unassign Remove the category from an access package. Body: { accessPackageId }
GET /api/category-assignments All category assignments as flat list (used by Matrix for column ordering).

Access Packages Page

Method Path Description
GET /api/access-packages Paginated access package list with category info.

GET /api/access-packages query parameters:

Parameter Type Default Description
search string Search displayName or catalog name (LIKE)
categoryId int Filter by category ID
uncategorized string Set to true to show only uncategorized packages
limit int 100 Page size (max 500)
offset int 0 Pagination offset

Filter Architecture

The UI uses a hybrid filtering approach for optimal performance:

┌─────────────────────────────────────────────────┐
│ Frontend (React)                                 │
│                                                  │
│  activeFilters: [{field, value}, ...]            │
│         │                                        │
│         ├── User attribute filters ──────────► Server-side (SQL WHERE)
│         │   (department, jobTitle, __userTag)     │
│         │                                        │
│         └── Relationship filters ──────────────► Client-side (JS filter)
│             (groupDisplayName, membershipType)   │
│                                                  │
│  Column discovery:                               │
│    /api/user-columns → full dataset values       │
│    Data rows → current page values               │
│                                                  │
│  Debounced fetch (400ms) on filter change        │
└─────────────────────────────────────────────────┘

Server-side filters (applied in SQL) are more efficient for large datasets — they reduce data before it reaches the browser. These include all columns from GraphUsers plus the virtual __userTag and __groupTag tag columns.

Client-side filters are applied in JavaScript after data is loaded. These include relationship-level fields like membershipType and groupDisplayName that come from the permission view rather than the users table.

All filters use parameterized SQL queries to prevent injection. Virtual tag columns (__userTag, __groupTag) are extracted from the filters object and translated to tag table subqueries before the main query runs.


Why FortigiGraph?

Identity Governance Insights You Can't Get from Entra ID

The Role Mining UI covers the most common analysis scenarios visually. For advanced or custom queries, FortigiGraph's SQL views give you full flexibility.

1. IST vs SOLL Analysis (As-Is vs Should-Be State)

The Problem: In Entra ID, you can't easily see the gap between what users should have (access package assignments) and what they actually have (direct group memberships).

What FortigiGraph Gives You:

The Matrix View's IST/SOLL toggle shows this visually. For custom analysis:

-- Find users with DIRECT group memberships when they should only have access through packages
SELECT * FROM vw_UnmanagedPermissions;

Use Cases:

  • Identify "backdoor" access that bypasses governance
  • Clean up direct assignments that should be managed by access packages
  • Audit compliance with access governance policies

2. Access Package Assignment Analysis

The Problem: Entra ID doesn't show you aggregate views of who has what through access packages, which packages are most used, or how assignments have changed over time.

-- Complete view of user permissions via access packages
SELECT * FROM vw_UserPermissionAssignmentViaAccessPackage;

-- All permission assignments (direct, indirect, eligible, owner)
SELECT * FROM vw_UserPermissionAssignments;

3. Approval Timeline Analysis

The Problem: Entra ID doesn't provide aggregate statistics on how long access requests take to approve.

-- Approval response times with buckets (< 1 hour, 1-4 hours, etc.)
SELECT * FROM vw_ApprovedRequestTimeline;

-- Find pending requests and how long they've been waiting
SELECT * FROM vw_PendingRequestTimeline WHERE hoursPending > 24;

-- Aggregate approval statistics
SELECT * FROM vw_RequestResponseMetrics;

4. Access Review Insights

The Problem: Entra ID shows individual review results, but doesn't aggregate patterns or completion rates.

-- Access package last review details
SELECT * FROM vw_AccessPackageLastReview;

-- Denied request patterns
SELECT * FROM vw_DeniedRequestTimeline;

5. Direct vs Governed Access

The Problem: You can't easily see which memberships are managed through governance vs direct assignment.

-- Complete membership analysis: Owner, Direct, Indirect, Eligible
SELECT * FROM vw_UserPermissionAssignments
WHERE memberId = 'user-guid-here';

-- Recursive group memberships with full paths
SELECT * FROM vw_GraphGroupMembersRecursive
WHERE groupId = 'group-guid-here'
ORDER BY depth;

6. Temporal/Historical Analysis

The Problem: Entra ID only shows current state. You can't answer "who had access on this date?"

-- Who had access to a specific group on January 15th?
SELECT * FROM GraphGroupMembers
FOR SYSTEM_TIME AS OF '2025-01-15 10:00:00'
WHERE groupId = 'your-group-id';

-- Track all changes for a specific user
SELECT userPrincipalName, department, ValidFrom, ValidTo
FROM GraphUsers FOR SYSTEM_TIME ALL
WHERE userPrincipalName = 'john.doe@contoso.com'
ORDER BY ValidFrom DESC;

Features

Core Capabilities

  • Guided Setup: New-FGConfig wizard creates all Azure resources and config in one go
  • Easy Authentication: Service principal and interactive auth with automatic token refresh
  • Azure SQL Integration: Temporal tables with automatic version history tracking
  • High-Performance Sync: SqlBulkCopy-based operations (20-50x faster than row-by-row)
  • Parallel Execution: Sync up to 6 entity types concurrently
  • Role Mining UI: Interactive web application for visual permission analysis

Data Sync

  • Users: All user properties including custom/extension attributes
  • Groups: Group details with security, type, and organization info
  • Memberships: Direct, transitive, PIM eligible, and owner relationships
  • Access Packages: Catalogs, packages, assignments, policies, requests, reviews
  • Automatic Schema Evolution: Add new columns without recreating tables

Analytical Views

FortigiGraph creates SQL views automatically for instant insights:

Group Membership Views (via Initialize-FGGroupMembershipViews):

  • vw_GraphGroupMembersRecursive - All memberships (direct + indirect) with paths
  • vw_UserPermissionAssignments - Comprehensive view with all types as separate rows: Owner, Direct, Indirect, Eligible (a user can have multiple types per group, e.g. Direct + Owner)

Access Package Views (via Initialize-FGAccessPackageViews):

  • vw_UserPermissionAssignmentViaAccessPackage - User permissions via access packages
  • vw_DirectGroupMemberships - Direct group memberships
  • vw_DirectGroupOwnerships - Direct group ownerships
  • vw_UnmanagedPermissions - IST vs SOLL gaps
  • vw_AccessPackageAssignmentDetails - Assignment details
  • vw_AccessPackageLastReview - Last review per package
  • vw_ApprovedRequestTimeline - Approval times with response buckets
  • vw_DeniedRequestTimeline - Denied request analysis
  • vw_PendingRequestTimeline - Aging pending requests
  • vw_RequestResponseMetrics - Aggregate approval statistics

Production Ready

  • Azure Automation: One-command setup with New-FGAzureAutomationAccount
  • Config-Driven: All settings in one JSON file
  • Secure Credentials: Encrypted credential storage using Windows DPAPI
  • Comprehensive Logging: Sync statistics logged to GraphSyncLog table

Config File

The config file drives all FortigiGraph operations. Create one with New-FGConfig or manually from the template in Config/tenantname.json.template.

Structure

{
  "Azure": {
    "TenantId": "yourtenant.onmicrosoft.com",
    "SubscriptionId": "your-subscription-id",
    "ResourceGroupName": "rg-fortigraph",
    "Location": "northeurope",
    "SQLServerName": "sql-fortigraph-xxxxx",
    "DatabaseName": "GraphData",
    "AdminUsername": "sqladmin",
    "AdminUserPassword_Encrypted": "...",
    "AutomationAccountName": "aa-fortigraph-xxxxx"
  },
  "Graph": {
    "TenantId": "yourtenant.onmicrosoft.com",
    "ClientId": "your-app-client-id",
    "ClientSecret_Encrypted": "..."
  },
  "Sync": {
    "Users": { "Enabled": true, "TableName": "GraphUsers", "AdditionalAttributes": [] },
    "Groups": { "Enabled": true, "TableName": "GraphGroups" },
    "GroupMembers": { "Enabled": true, "TableName": "GraphGroupMembers" },
    "GroupEligibleMembers": { "Enabled": true },
    "GroupOwners": { "Enabled": true, "TableName": "GraphGroupOwners" },
    "Catalogs": { "Enabled": true, "TableName": "GraphCatalogs" },
    "AccessPackages": { "Enabled": true },
    "AccessPackageAssignments": { "Enabled": true },
    "AccessPackageResourceRoleScopes": { "Enabled": true },
    "AccessPackageAssignmentPolicies": { "Enabled": true },
    "AccessPackageAssignmentRequests": { "Enabled": true },
    "AccessPackageAccessReviews": { "Enabled": true },
    "Views": { "Enabled": true },
    "ParallelExecution": true
  }
}

Authentication

Service Principal (Automated/Scheduled Tasks)

# Using config file (recommended)
Get-FGAccessToken -ConfigFile '.\Config\mycompany.json'

# Or with explicit parameters
Get-FGAccessToken -TenantId "contoso.onmicrosoft.com" -ClientId "app-client-id" -ClientSecret "secret"

Interactive (User Delegation)

Get-FGAccessTokenInteractive -TenantId "contoso.onmicrosoft.com" -ClientId "app-client-id"

Required Permissions

Permission Purpose
User.Read.All Read all users
Group.Read.All Read all groups
GroupMember.Read.All Read group memberships
Directory.Read.All Read directory data
EntitlementManagement.Read.All Read access packages, catalogs, assignments
AccessReview.Read.All Read access review decisions
AuditLog.Read.All Read sign-in activity (used by user sync)

New-FGConfig sets up all these permissions automatically when creating a new App Registration.


Data Synchronization

Orchestrated Sync

# Sync everything based on config file settings
Start-FGSync -ConfigFile '.\Config\mycompany.json'

Start-FGSync handles:

  • Authentication (always gets a fresh token)
  • SQL connection with firewall management
  • Parallel execution of all enabled sync types
  • Performance index creation
  • Analytical view creation
  • Summary report with statistics

Individual Sync Commands

# Users (with extra attributes)
Sync-FGUser -AdditionalAttributes @('officeLocation', 'city', 'employeeType')

# Groups
Sync-FGGroup

# Memberships
Sync-FGGroupMember              # Direct memberships
Sync-FGGroupTransitiveMember    # Transitive (includes nested)
Sync-FGGroupEligibleMember      # PIM eligible memberships
Sync-FGGroupOwner               # Group owners

# Access Packages
Sync-FGCatalog
Sync-FGAccessPackage
Sync-FGAccessPackageAssignment
Sync-FGAccessPackageResourceRoleScope
Sync-FGAccessPackageAssignmentPolicy
Sync-FGAccessPackageAssignmentRequest
Sync-FGAccessPackageAccessReview

Automatic Schema Evolution

Add new attributes without recreating the table:

# First run - default attributes
Sync-FGUser

# Later - add new attributes (columns added automatically)
Sync-FGUser -AdditionalAttributes @('employeeType', 'officeLocation')

SQL Management

# Connect using config file
Connect-FGSQLServer -ConfigFile '.\Config\mycompany.json'

# List tables
Get-FGSQLTable

# Query data
Invoke-FGSQLQuery -Query "SELECT * FROM GraphUsers WHERE department = 'IT'"
$count = Invoke-FGSQLQuery -Query "SELECT COUNT(*) FROM GraphUsers" -AsScalar

# Clear table (preserves history)
Clear-FGSQLTable -TableName "GraphUsers_Test"

# Clear table and history
Clear-FGSQLTable -TableName "GraphUsers_Test" -DeleteHistory -Force

Temporal Tables & Historical Queries

All synced data uses SQL Server temporal tables for automatic change tracking.

-- Current data
SELECT * FROM GraphUsers;

-- Point-in-time query
SELECT * FROM GraphUsers FOR SYSTEM_TIME AS OF '2025-06-15 10:00:00';

-- All history
SELECT * FROM GraphUsers FOR SYSTEM_TIME ALL
WHERE userPrincipalName = 'john@contoso.com'
ORDER BY ValidFrom DESC;

-- Changes in the last 30 days
SELECT * FROM GraphGroupMembers FOR SYSTEM_TIME ALL
WHERE ValidFrom >= DATEADD(DAY, -30, GETDATE())
ORDER BY ValidFrom DESC;

Azure Automation

Set up automated scheduled syncs with a single command:

New-FGAzureAutomationAccount -ConfigFile '.\Config\mycompany.json'

What it creates:

  • Azure Automation Account
  • Encrypted variables for Graph and SQL credentials
  • Runbooks for each sync type (Users, Groups, Members, Catalogs, Access Packages, etc.)
  • Daily schedules (optional, configurable time zone)
  • SQL firewall rule for Azure services

Memory considerations: Azure Automation sandbox has a 400 MB limit. Group member sync uses -UseBatching mode for constant memory usage.

Post-setup:

  1. Import FortigiGraph module via Azure Portal > Automation Account > Modules > Browse Gallery
  2. Wait for module imports to complete
  3. Test runbooks manually before enabling schedules

Attribute Mapping Discovery

Discover and document attribute mappings across your identity infrastructure:

# Get all apps with provisioning configured
$apps = Get-FGServicePrincipalWithSync -IncludeSchema

# Extract attribute mappings
$mappings = Get-FGAttributeMapping -ServicePrincipalWithSync $apps

# Export for analysis
$mappings | Export-Csv -Path "attribute-mappings.csv" -NoTypeInformation

Works with HR provisioning (Workday, SuccessFactors), Azure AD Connect Cloud Sync, and SCIM applications.


Repository Structure

FortigiGraph/
├── Functions/              # All PowerShell functions
│   ├── Base/               # Authentication & HTTP operations (20 functions)
│   ├── Generic/            # Graph API wrappers (49 functions)
│   ├── Specific/           # Business logic helpers (9 functions)
│   ├── SQL/                # Azure SQL operations (24 functions)
│   ├── Sync/               # Data synchronization (14 functions)
│   └── Automation/         # Azure Automation management (4 functions)
├── UI/                     # Role Mining Web Application
│   ├── backend/            # Node.js + Express API server
│   └── frontend/           # React + Vite + Tailwind
├── Config/                 # Configuration templates
│   └── tenantname.json.template
├── _Build/                 # Build and publishing scripts
├── _Test/                  # Testing scripts and documentation
├── FortigiGraph.psm1       # Module entry point
├── FortigiGraph.psd1       # Module manifest
└── README.md

Total: 120 functions


Troubleshooting

Debug Mode

$Global:DebugMode = 'G'     # GET requests
$Global:DebugMode = 'P'     # POST/PATCH requests
$Global:DebugMode = 'D'     # DELETE requests
$Global:DebugMode = 'T'     # Token operations
$Global:DebugMode = 'GP'    # Multiple categories

Common Issues

Issue Solution
SQL connection fails Connect-FGSQLServer -ConfigFile config.json (updates firewall automatically)
"No Access Token found" Run Get-FGAccessToken -ConfigFile config.json
Permission errors after changing app Start-FGSync always gets a fresh token; for manual use run Get-FGAccessToken again
Temporal table schema error Don't modify tables directly; use Sync-FG* functions which handle schema changes
Can't truncate temporal table Use DELETE or Clear-FGSQLTable instead of TRUNCATE

Requirements

  • PowerShell: 5.1 or later (7+ recommended)
  • Azure: Subscription with Contributor access
  • Modules: Az PowerShell module (Install-Module Az -Scope CurrentUser)
  • Permissions: See Required Permissions table

Support


License

MIT License - see LICENSE for details.


Author: Wim van den Heijkant | Company: Fortigi GitHub: github.com/Fortigi/FortigiGraph | PowerShell Gallery: FortigiGraph

About

PowerShell Module to Query the Microsoft Graph created by Fortigi

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors