Skip to content

Conversation

@WimvandenHeijkant
Copy link
Contributor

Summary

This PR adds comprehensive Azure Automation runbook management capabilities and secure credential handling to FortigiGraph. It includes new functions for starting, monitoring, and listing automation runbooks, plus utilities for securely storing and retrieving encrypted credentials from JSON configuration files.

Key Changes

New Automation Management Functions

  • Start-FGAutomationRunbook: Triggers Azure Automation runbooks with optional wait-for-completion and timeout support
  • Get-FGAutomationJob: Monitors automation job status with filtering by runbook name, status, and time range
  • Get-FGAutomationRunbook: Lists available runbooks with filtering for FortigiGraph sync operations
  • New-FGAzureAutomationAccount: Creates and configures Azure Automation accounts with required modules and runbooks

Secure Credential Management

  • Get-FGSecureConfigValue: Retrieves configuration values with DPAPI encryption support, automatic prompting, and plaintext-to-encrypted migration
  • Clear-FGSecureConfigValue: Removes stored credentials from config files
  • Enhanced Get-FGAccessToken: Now supports reading credentials from config files via Get-FGSecureConfigValue

Configuration & Security

  • Added .gitignore to exclude sensitive config files and Claude workspace directories
  • Credentials are encrypted using Windows DPAPI (user and machine-specific)
  • Automatic migration from plaintext to encrypted storage

API Request Improvements

  • Invoke-FGGetRequest: Added progress reporting for multi-page API responses, improved token refresh during pagination
  • Invoke-FGGetRequestToFile: Fixed JSON formatting issues and improved array handling

Implementation Details

  • All automation functions support both config file and explicit parameter modes
  • Runbook names are validated against a predefined list of FortigiGraph sync operations
  • Job monitoring includes human-readable duration formatting and status color coding
  • Secure config functions use dot-notation paths (e.g., "Azure.AdminUserPassword") for flexible property access
  • Token refresh is now checked during long-running pagination operations to prevent mid-request expiration

https://claude.ai/code/session_01UT5CbdJGD9kxhyfVziTpTD

WimvandenHeijkant and others added 30 commits December 19, 2025 19:59
…ioning

This commit adds comprehensive Azure SQL Server integration to the FortigiGraph module, enabling users to store Microsoft Graph data in SQL with automatic version history tracking.

New Base Functions:
- New-FGAzureSQLServer: Provisions Azure SQL Server and Database with firewall configuration
- Connect-FGSQLServer: Establishes SQL Server connection with credential management
- Connect-FGSQLServerFromAzure: Smart wrapper with automatic firewall updates and Azure integration
- Initialize-FGSQLTable: Creates temporal tables with automatic history tracking
- Test-FGSQLConnection: Verifies SQL connection and displays server information
- Invoke-FGSQLQuery: Execute SQL queries from PowerShell command line

New Generic Functions:
- Sync-FGUser: Syncs Microsoft Graph users to SQL with automatic schema detection

Key Features:
- Temporal tables with ValidFrom/ValidTo columns for automatic version history
- Azure context confirmation before operations
- Automatic server name normalization and validation
- Firewall rule management with current IP detection
- Credential caching for session persistence
- Connection validation before claiming "already connected"
- Automatic schema evolution when adding new attributes
- Type-aware NULL handling for change detection
- Transaction-based sync for optimal performance (40-50 users/sec)
- Progress tracking with timestamps and rate display
- Deletion handling for removed users
- Support for 16 default user attributes plus custom/additional attributes
- Manager and signInActivity expansion handling

Documentation:
- Comprehensive README.md with function documentation
- Quick start guide
- SQL query examples for temporal data
- PowerShell query examples using Invoke-FGSQLQuery
- Best practices and troubleshooting guide

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This refactoring improves the architecture of the SQL integration by:

1. Created Invoke-FGSQLCommand helper function:
   - Centralizes SQL connection lifecycle management (open/close/dispose)
   - Eliminates duplicate connection handling code across all SQL functions
   - Provides consistent error handling and cleanup
   - Uses ScriptBlock pattern for maximum flexibility

2. Refactored existing functions to use the helper:
   - Connect-FGSQLServer: Now uses helper for connection testing
   - Connect-FGSQLServerFromAzure: Calls Test-FGSQLConnection instead of duplicating test logic
   - Test-FGSQLConnection: Uses helper for connection and query execution
   - Initialize-FGSQLTable: Uses helper for all SQL operations
   - Sync-FGUser: Uses helper for both table/schema checks and sync operations

Key improvements:
- Functions now focus on their business logic, not connection management
- Connect-FGSQLServerFromAzure properly delegates to Test-FGSQLConnection
- No more duplicate connection open/close/dispose patterns
- Better separation of concerns following the "wrapper" pattern
- Maintains all existing functionality while reducing code duplication

This follows the DRY (Don't Repeat Yourself) principle and makes the codebase more maintainable.
Added comprehensive documentation about the refactored architecture:

1. New "Architecture & Design" section:
   - Explains the Invoke-FGSQLCommand helper function
   - Documents the benefits of centralized connection management
   - Lists clear responsibilities for each SQL function
   - Shows how functions delegate to each other (wrapper pattern)
   - Emphasizes DRY principle and separation of concerns

2. Added Invoke-FGSQLQuery documentation:
   - Function description and parameters
   - Multiple usage examples (queries, exports, grid view)
   - Shows how to query current data and temporal history

This documentation helps developers understand:
- Why the code is organized this way
- How the functions work together
- The benefits of the architectural approach
- How to use the SQL query function effectively
…naming

Major changes:
1. Created dedicated SQL/ folder for all SQL-related functions
2. Renamed Connect-FGSQLServerFromAzure -> Connect-FGSQLServer (main function)
3. Renamed Connect-FGSQLServer -> New-FGSQLConnection (low-level helper)
4. Removed Invoke-FGSQLQuery (redundant, use Invoke-FGSQLCommand instead)
5. Fixed AutoConnect bug in New-FGAzureSQLServer to use correct parameters

Structure improvements:
- SQL functions now separated from Graph API functions
- Clearer naming: users call Connect-FGSQLServer for best experience
- Better separation of concerns: one command execution function
- Updated all documentation and references in README

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This documentation provides a complete guide for AI assistants working with the FortigiGraph codebase, including:

Architecture & Features:
- Microsoft Graph API integration with automatic pagination and token refresh
- NEW: Azure SQL integration with temporal versioning for automatic change tracking
- NEW: Sync-FGUser function for syncing Graph users to SQL with schema detection
- Comprehensive testing infrastructure with secure credential storage

Code Organization:
- Base/ - Authentication and HTTP operations (17 functions)
- Generic/ - Graph API operations including Sync-FGUser (47 functions)
- SQL/ - Azure SQL operations with helper pattern (10 functions)
- Specific/ - High-level idempotent helpers (10 functions)
- _Test/ - Integration tests and secure credentials (4 scripts)

Key Design Patterns:
- Invoke-FGSQLCommand helper for consistent SQL connection management
- Temporal tables for automatic change tracking with zero code overhead
- Automatic schema evolution (add columns without recreating tables)
- Transaction-based syncing with progress tracking

Development Guidelines:
- Function templates and examples for Graph API and SQL operations
- Best practices for temporal table handling
- Testing infrastructure documentation
- DO/DON'T lists for common pitfalls

Total: 88 functions, 4,753 lines of code
This commit significantly improves code organization and reduces duplication
by introducing reusable helper functions and a new group sync capability.

New Features:
- Sync-FGGroup: Syncs Microsoft Graph groups to SQL with temporal versioning
  * 20+ default group attributes (id, displayName, mail, type, security, etc.)
  * Support for on-premises sync attributes
  * Owner expansion (ownerId from first owner)
  * Does NOT sync members (that's for a separate function)
  * Same pattern as Sync-FGUser for consistency

SQL Helper Functions (improves code reuse):
1. Test-FGSQLTableExists: Check if table exists in database
2. Get-FGSQLTableSchema: Get list of columns from existing table
3. Add-FGSQLTableColumn: Add columns to temporal table with proper versioning
4. New-FGSQLMergeStatement: Build optimized MERGE statements with change detection
5. ConvertTo-FGSQLParameter: Convert Graph values to SQL parameters with type handling

Code Improvements:
- Refactored Sync-FGUser to use new helper functions
- Reduced code duplication by ~200 lines
- Consistent pattern between Sync-FGUser and Sync-FGGroup
- Better separation of concerns (schema mgmt, type conversion, SQL building)
- More maintainable and testable code

Group Attributes Synced by Default:
- Identity: id, displayName, description, mail, mailNickname
- Type: mailEnabled, securityEnabled, groupTypes, visibility
- Metadata: createdDateTime, renewedDateTime, expirationDateTime
- Advanced: isAssignableToRole, membershipRule, membershipRuleProcessingState
- On-Premises: onPremisesSamAccountName, onPremisesSyncEnabled, etc.
- Owner: ownerId (from expanded owners collection)

Total New Files: 6 (5 SQL helpers + 1 sync function)
Lines of Code: ~800 lines added, ~200 lines removed through refactoring
This commit adds the ability to sync group membership relationships to SQL
with temporal versioning and composite primary keys.

New Features:
- Sync-FGGroupMember: Syncs many-to-many group membership relationships
  * Composite primary key (groupId, memberId) for unique constraint
  * Temporal table for tracking membership changes over time
  * Iterates through each group to fetch all members (required pattern)
  * Handles all member types: users, groups, devices, service principals
  * Optional transitive member support (nested groups)
  * Deletion handling (removes memberships that no longer exist)
  * Progress tracking with rate display
  * Transaction-based for performance

Attributes Synced:
- groupId (UNIQUEIDENTIFIER) - Group identifier
- memberId (UNIQUEIDENTIFIER) - Member identifier
- memberType (NVARCHAR) - Type from @odata.type (e.g., #microsoft.graph.user)

Enhanced Helper Functions:
- New-FGSQLMergeStatement: Updated to support composite primary keys
  * Accepts array of primary key columns
  * Builds proper ON clause with multiple key conditions
  * Excludes all PK columns from UPDATE SET statement
  * Works with both single and composite keys

Use Cases:
- Track who is in which groups over time
- Audit membership changes with temporal queries
- Join with GraphUsers and GraphGroups tables
- Query membership at any point in time

Example Usage:
  # Sync all group memberships
  Sync-FGGroupMember

  # Sync only security groups
  Sync-FGGroupMember -Filter "securityEnabled eq true"

  # Sync specific groups
  Sync-FGGroupMember -GroupIds @('group-id-1', 'group-id-2')

  # Include nested group members
  Sync-FGGroupMember -IncludeTransitiveMembers

Performance Notes:
- Must iterate through each group individually (Graph API limitation)
- Shows progress every 10 groups processed
- Progress tracking every 1000 memberships synced
- Transaction-based for optimal performance
- Typical rate: ~5-10 groups/sec depending on member counts

Total Files: 2 (1 new, 1 modified)
Lines of Code: ~380 new lines in Sync-FGGroupMember
This commit adds dedicated support for syncing transitive (nested) group
memberships separately from direct memberships.

New Features:
- Sync-FGGroupTransitiveMember: Syncs ALL group memberships including nested
  * Same structure as Sync-FGGroupMember (groupId, memberId, memberType)
  * Uses /transitiveMembers endpoint instead of /members
  * Default table: GraphGroupTransitiveMembers (separate from direct members)
  * Composite primary key (groupId, memberId)
  * Temporal versioning for tracking changes over time
  * Iterates through each group to fetch all transitive members
  * Progress tracking and transaction-based syncing

Use Cases:
- Track ALL users with access through nested groups
- Separate tracking of direct vs transitive memberships
- Audit complete access paths through group hierarchies
- Compare direct vs effective memberships over time

Difference from Sync-FGGroupMember:
- Sync-FGGroupMember: Direct members only
- Sync-FGGroupTransitiveMember: All members including nested (transitive)

Example Usage:
  # Sync all transitive memberships
  Sync-FGGroupTransitiveMember

  # Sync only security groups (transitive)
  Sync-FGGroupTransitiveMember -Filter "securityEnabled eq true"

  # Sync specific groups (transitive)
  Sync-FGGroupTransitiveMember -GroupIds @('group-id-1', 'group-id-2')

Query Examples:
  -- Find all effective members of a group (including nested)
  SELECT * FROM GraphGroupTransitiveMembers
  WHERE groupId = 'group-guid'

  -- Compare direct vs transitive memberships
  SELECT
    t.groupId,
    t.memberId,
    CASE WHEN d.memberId IS NULL THEN 'Nested' ELSE 'Direct' END AS MembershipType
  FROM GraphGroupTransitiveMembers t
  LEFT JOIN GraphGroupMembers d
    ON t.groupId = d.groupId AND t.memberId = d.memberId
  WHERE t.groupId = 'group-guid'

  -- Find users who have access through nested groups only
  SELECT t.*
  FROM GraphGroupTransitiveMembers t
  LEFT JOIN GraphGroupMembers d
    ON t.groupId = d.groupId AND t.memberId = d.memberId
  WHERE d.memberId IS NULL

Benefits:
- Maintain separate tables for direct and transitive memberships
- Track changes to nested group structures over time
- Understand full access paths and effective permissions
- Audit nested group membership changes

Total Files: 1 new file
Lines of Code: ~380 lines
This commit adds a helper function to create SQL views that simplify
analyzing direct vs indirect/nested group memberships.

New Features:
- Initialize-FGGroupMembershipViews: Creates two analytical SQL views
  * Automatically creates views based on membership tables
  * Supports custom table names
  * Drop and recreate capability

Views Created:

1. vw_GraphGroupNestedMembers
   - Shows ONLY members who have indirect/nested access
   - Excludes direct members
   - Useful for: Finding hidden access paths through nested groups
   - Columns: groupId, memberId, memberType, ValidFrom, ValidTo

2. vw_GraphGroupMembershipType
   - Shows ALL members (both direct and indirect)
   - Includes membershipType column: "Direct" or "Indirect"
   - Useful for: Complete membership analysis with type indicator
   - Columns: groupId, memberId, memberType, membershipType, ValidFrom, ValidTo

Usage:
  # Create the views (run once after syncing membership data)
  Initialize-FGGroupMembershipViews

  # Recreate views
  Initialize-FGGroupMembershipViews -DropIfExists

  # Use custom table names
  Initialize-FGGroupMembershipViews -DirectMembersTable "MyDirectMembers" -TransitiveMembersTable "MyTransitiveMembers"

Query Examples:

  -- Find all nested members of a specific group
  SELECT * FROM vw_GraphGroupNestedMembers
  WHERE groupId = 'group-guid'

  -- Count direct vs indirect members per group
  SELECT
    groupId,
    membershipType,
    COUNT(*) AS MemberCount
  FROM vw_GraphGroupMembershipType
  GROUP BY groupId, membershipType

  -- Find users with only indirect access
  SELECT DISTINCT
    v.memberId,
    u.userPrincipalName,
    u.displayName
  FROM vw_GraphGroupNestedMembers v
  INNER JOIN GraphUsers u ON v.memberId = u.id
  WHERE v.memberType = '#microsoft.graph.user'

  -- Compare membership types across all groups
  SELECT
    g.displayName AS GroupName,
    v.membershipType,
    COUNT(*) AS Count
  FROM vw_GraphGroupMembershipType v
  INNER JOIN GraphGroups g ON v.groupId = g.id
  GROUP BY g.displayName, v.membershipType
  ORDER BY g.displayName, v.membershipType

Benefits:
- No need to write complex LEFT JOIN queries
- Consistent logic across all queries
- Easy to understand membership hierarchy
- Temporal data (ValidFrom/ValidTo) preserved for point-in-time queries
- Works seamlessly with GraphUsers and GraphGroups tables

Requirements:
- GraphGroupMembers table (from Sync-FGGroupMember)
- GraphGroupTransitiveMembers table (from Sync-FGGroupTransitiveMember)

Total Files: 1 new file
Lines of Code: ~160 lines
- Add Sync-FGGroupEligibleMember function for PIM eligible group memberships
  - Syncs eligible members from PIM-enabled groups (isAssignableToRole = true)
  - Uses /identityGovernance/privilegedAccess/group/eligibilitySchedules endpoint
  - Creates GraphGroupEligibleMembers table with composite key (groupId, memberId)
  - Includes memberType for filtering

- Update Initialize-FGGroupMembershipViews with eligible member support
  - Add vw_GraphGroupEligibleMembers view (shows only PIM eligible members)
  - Enhance vw_GraphGroupMembershipType to include Direct/Indirect/Eligible indicator
  - Use UNION to include eligible-but-not-active members
  - Support both PIM and non-PIM environments gracefully
- Add group sync features to Features section
- Add Quick Start examples for group and membership syncing
- Document Sync-FGGroup with 20+ default attributes
- Document Sync-FGGroupMember for direct memberships
- Document Sync-FGGroupTransitiveMember for nested memberships
- Document Sync-FGGroupEligibleMember for PIM eligible memberships
- Document Initialize-FGGroupMembershipViews with all 3 views
- Add query examples for membership analysis
- Explain composite primary keys and many-to-many relationships
- Provide use cases and examples for each function
Add Tests 16-22 to integration test suite:
- Test 16: Sync-FGGroup with default properties
- Test 17: Sync-FGGroupMember (direct memberships)
  - Verifies composite primary key (groupId, memberId)
  - Validates table structure
- Test 18: Sync-FGGroupTransitiveMember (nested memberships)
  - Compares direct vs transitive membership counts
  - Shows additional nested members
- Test 19: Sync-FGGroupEligibleMember (PIM memberships)
  - Checks for PIM-enabled groups first
  - Gracefully skips if no PIM groups or PIM not available
- Test 20: Initialize-FGGroupMembershipViews
  - Creates all 3 membership analysis views
  - Verifies view creation
- Test 21: Query group membership views
  - Tests nested members view
  - Shows membership type breakdown (Direct/Indirect/Eligible)
  - Displays sample memberships
- Test 22: Group sync summary
  - Comprehensive summary of groups, direct, and transitive memberships
  - Shows sync timestamps

Features:
- All tests include proper data verification
- Graceful handling of optional PIM functionality
- Detailed output with colored formatting
- Resource registration for cleanup
- Incremental test numbering (16-23 for cleanup)
Changes:
- Remove ownerId from Sync-FGGroup function
  - Removed from default attributes list
  - Removed from type mapping
  - Removed expand logic for owners collection
  - Removed special processing for first owner

- Add new Sync-FGGroupOwner function
  - Dedicated many-to-many table for group ownership
  - Composite primary key (groupId, ownerId)
  - Syncs all owners (not just first owner)
  - Temporal versioning for ownership changes
  - Follows same pattern as Sync-FGGroupMember

- Update Initialize-FGGroupMembershipViews to include owners
  - Add OwnersTable parameter (default: "GraphGroupOwners")
  - Update vw_GraphGroupMembershipType view to include "Owner" type
  - Owner takes priority in membership type (Owner > Direct > Eligible > Indirect)
  - Add UNION for owners not in transitive members
  - Update view descriptions and output to reflect owner support
  - Gracefully handles missing owners table (optional like eligible)

Benefits:
- More accurate representation (groups can have multiple owners)
- Consistent pattern with other many-to-many relationships
- Temporal tracking of ownership changes over time
- Views now show complete picture: Owner/Direct/Indirect/Eligible
Bug: The vw_GraphGroupMembershipType view was referencing o.ownerId
in the CASE statement even when the owners table didn't exist, causing:
"The multi-part identifier 'o.ownerId' could not be bound."

Fix: Conditionally build the CASE statement to only reference o.ownerId
when $ownersExists is true.

Before: CASE always checked o.ownerId first (fails if table missing)
After: CASE only checks o.ownerId when owners table exists

This allows the view to be created successfully in environments without
the GraphGroupOwners table, showing only Direct/Indirect (and Eligible
if that table exists).
Create Test-Integration-Fast.ps1 for rapid iteration during development:

Features:
- Validates existing SQL Server instead of creating new one
- Clears all existing tables and views
- Runs all same tests as full integration test
- Keeps SQL Server by default for next run
- Optional -RemoveServer flag to clean up

Benefits:
- Saves 3-5 minutes per test run (no server/DB creation)
- Perfect for development workflow
- Reuses Azure resources efficiently

Usage:
1. First run: Test-Integration.ps1 -SkipCleanup
   (creates server, ~10 minutes)

2. Subsequent runs: Test-Integration-Fast.ps1 -ConfigFile config.json
   (reuses server, ~5 minutes)

3. Final cleanup: Test-Integration-Fast.ps1 -ConfigFile config.json -RemoveServer
   (removes all resources)

The fast test:
- Validates SQL Server exists (fails fast if not found)
- Clears all tables (with proper temporal table handling)
- Drops all views
- Runs Tests 7-22 (all sync and query tests)
- Preserves server for next iteration
Problem:
- Test 6 cleared table data but dropped all views
- Tests 7-9 tried to create tables that already existed
- Test 14 failed because helper views were missing

Solution:
- Remove view dropping logic from Test 6 (preserve views)
- Skip Tests 7-9 entirely (tables already exist)
- Jump directly to sync tests which use existing infrastructure

This aligns with the fast test's purpose: reuse existing SQL Server
infrastructure to speed up testing.
Test 14 queries temporal history views (vw_*_AllHistory) which are
only created by Initialize-FGSQLTable during table creation.

Since the fast test:
- Skips table creation (tables already exist)
- Only syncs data to existing tables
- Never calls Initialize-FGSQLTable

The helper views don't exist in the fast test scenario. This test
validates SQL Server temporal features which are already covered
in the full integration test. The fast test focuses on sync speed.
Problem:
- All sync functions used Invoke-RestMethod directly
- No token validation before API calls
- In large environments, sync operations take a long time
- Access tokens expire during long-running operations
- Subsequent API calls fail with 401 Unauthorized

Solution:
- Replace all Invoke-RestMethod calls with Invoke-FGGetRequest
- Invoke-FGGetRequest automatically checks token validity before each call
- Auto-refreshes expired tokens using stored credentials
- Simplifies pagination handling (now automatic)

Changes:
- Sync-FGUser: Replace manual pagination with Invoke-FGGetRequest
- Sync-FGGroup: Replace manual pagination with Invoke-FGGetRequest
- Sync-FGGroupMember: Fix 3 API call locations (groups + members loop)
- Sync-FGGroupTransitiveMember: Fix 3 API call locations
- Sync-FGGroupEligibleMember: Fix 4 API call locations (groups + nested loops)
- Sync-FGGroupOwner: Fix 3 API call locations (groups + owners loop)

Benefits:
- Token automatically refreshed every time it expires
- Works reliably in large environments with thousands of groups
- Cleaner code with automatic pagination
- Consistent error handling across all sync functions
Critical Bug Fix in Invoke-FGGetRequest:
========================================
Problem:
- Token was captured at function start (line 13: $AccessToken = $Global:AccessToken)
- When token expired, it was refreshed and stored in $Global:AccessToken
- BUT the function continued using the OLD local $AccessToken variable
- Result: Token refresh happened but API calls still used expired token!

Solution:
- Removed early token capture (line 13)
- Added token capture AFTER refresh check (line 44)
- Added token validation in pagination loop (tokens can expire during long pagination)
- Now uses refreshed token for all API calls

Impact:
- Fixes "token is expired" errors in large environments
- Especially critical for transitive member sync (9318 groups in test)
- Token is now checked and refreshed before EVERY page request

Test Summary Enhancement:
========================
Problem:
- Summary only showed Groups, Direct, and Transitive memberships
- Missing Eligible Memberships (PIM) and Group Ownerships

Solution:
- Dynamic query that checks if tables exist
- Adds Eligible Memberships (PIM) if table exists
- Adds Group Ownerships if table exists
- Applied to both Test-Integration.ps1 and Test-Integration-Fast.ps1

New Summary Output:
Groups                      9318
Direct Memberships         17343
Transitive Memberships     XXXXX
Eligible Memberships (PIM) XXXXX  ← NEW
Group Ownerships           XXXXX  ← NEW
Problem:
- Some environments have Azure resources in one Entra ID tenant
- And Microsoft Graph API in a different Entra ID tenant
- Tests always used Graph.TenantId for Azure operations
- This caused Azure connection failures in multi-tenant scenarios

Solution:
- Add optional Azure.TenantId to config file
- If Azure.TenantId is specified, use it for Azure operations
- If not specified, fall back to Graph.TenantId (backward compatible)
- Updated both Test-Integration.ps1 and Test-Integration-Fast.ps1

Config File Structure:
{
  "Azure": {
    "TenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",  ← NEW (optional)
    "SubscriptionId": "...",
    "ResourceGroupName": "..."
  },
  "Graph": {
    "TenantId": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
    "ClientId": "..."
  }
}

If Azure.TenantId is omitted, Graph.TenantId is used for both (backward compatible).
Documentation Updates:
- README-Integration-Tests.md: Added Azure.TenantId to config example
- Added explanation of when to use separate Azure tenant ID
- Clarified backward compatibility (optional field)

Template Updates:
- config.test.json.template: Added Azure.TenantId field
- Included inline comment explaining when it's needed
- Made it clear the field is optional

Users can now:
- Copy template and understand multi-tenant configuration
- See clear examples in the README
- Understand backward compatibility (single-tenant still works)
WimvandenHeijkant and others added 30 commits February 4, 2026 18:46
…etup

Graph.ClientSecret and Azure.AdminUserPassword are now read using
Get-FGSecureConfigValue which supports:
- DPAPI-encrypted values (ClientSecret_Encrypted, AdminUserPassword_Encrypted)
- Automatic decryption
- Prompting if not found
- Auto-migration from plaintext to encrypted

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added -RuntimeVersion "7.2" to Import-AzAutomationRunbook to ensure
runbooks use PowerShell 7.2 instead of defaulting to 5.1.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add -RuntimeVersion parameter with ValidateSet("5.1", "7.2") and default "7.2"
- Use variable instead of hardcoded value in Import-AzAutomationRunbook
- Runbooks now default to PowerShell 7.2 runtime without requiring explicit parameter

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Runbooks are now created by default (no need to specify -CreateRunbooks)
- Add -SkipRunbooks switch to skip runbook creation if needed
- Updated documentation and examples

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The -RuntimeVersion parameter doesn't exist in Import-AzAutomationRunbook.
Use -Type parameter with "PowerShell72" for PS 7.2 or "PowerShell" for PS 5.1.

- Rename parameter from RuntimeVersion to RunbookType
- Default to "PowerShell72" for PowerShell 7.2 runtime
- Update documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Check if SQL Server allows Azure services (0.0.0.0 firewall rule)
- If not configured, prompt user for approval before adding the rule
- Automatically detect SQL Server's resource group (may differ from Automation RG)
- Show clear warning if user declines or if configuration fails

This is required for Azure Automation runbooks to connect to SQL Server.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use case-insensitive comparison (-ieq) when finding SQL Server
- Use actual server name from Azure ($sqlServer.ServerName) instead of config value
- Azure normalizes server names to lowercase, but config may have mixed case

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…lter)

- Extract sync configuration from config file (Users.AdditionalAttributes, Users.Filter, Groups.Filter)
- Store sync config as Automation Variables (SyncUsersAdditionalAttributes, SyncUsersFilter, SyncGroupsFilter)
- Update Sync-FGUsers runbook to read and apply additional attributes and filter
- Update Sync-FGGroups runbook to read and apply filter
- Variables stored as comma-separated strings, parsed in runbook

This allows runbooks to sync the same attributes configured in the config file.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
New command that walks users through creating a FortigiGraph config file
step by step with sensible defaults. Passwords and secrets are automatically
encrypted with DPAPI. Supports -Quick switch for essentials-only setup.

The generated config works with Get-FGAccessToken, Connect-FGSQLServer,
and Start-FGSync.

https://claude.ai/code/session_01BSwCZCjSPByVYtMwUBjc2o
Instead of asking users to type GUIDs manually, the config wizard now:
- Uses Connect-AzAccount (or reuses existing session)
- Lists subscriptions to pick from
- Lists resource groups to pick from
- Lists existing SQL servers and databases to pick from
- Auto-discovers tenant ID from the Azure context

Only the Graph API Client ID/Secret still need manual entry since those
come from the App Registration.

https://claude.ai/code/session_01BSwCZCjSPByVYtMwUBjc2o
- Tenant ID now taken directly from Connect-AzAccount context (no
  separate tenant prompt or Graph TenantId question)
- SQL admin password auto-generated as a 24-char cryptographically
  random complex password by default, with option to enter your own
- Uses RandomNumberGenerator for secure password generation
- Cleaner login flow: shows account + tenant, asks to confirm

https://claude.ai/code/session_01BSwCZCjSPByVYtMwUBjc2o
When no existing SQL servers are found, suggests a name like
sql-fortigraph-x7k2m instead of requiring manual input. User can
accept with Enter or type their own. Azure SQL names must be globally
unique since they become DNS names.

https://claude.ai/code/session_01BSwCZCjSPByVYtMwUBjc2o
Users can now choose to create a new App Registration directly from the
config wizard. The wizard will:
- Create the App Registration (New-AzADApplication)
- Create the Service Principal (New-AzADServicePrincipal)
- Generate a 2-year client secret (New-AzADAppCredential)
- Add all required Graph API permissions:
  User.Read.All, Group.Read.All, GroupMember.Read.All,
  Directory.Read.All, EntitlementManagement.Read.All,
  AccessReview.Read.All
- Provide a direct Azure Portal URL for admin consent

Falls back gracefully to manual entry if the user lacks permissions
to create apps. Existing app option still available.

https://claude.ai/code/session_01BSwCZCjSPByVYtMwUBjc2o
The recursive view (vw_GraphGroupMembersRecursive) replaces the need
for syncing transitive members separately, saving ~75% sync time.
Removed from New-FGConfig prompts, config template, and example config.
The sync function itself remains for backwards compatibility.

https://claude.ai/code/session_01BSwCZCjSPByVYtMwUBjc2o
- Create resource group, SQL Server + database, and Automation Account during setup
- Add AdditionalAttributes empty array for Groups sync config
- Add AutomationAccountName with default "aa-fortigraph" to config output
- Update module version to 2.1.20260209.1200

https://claude.ai/code/session_01BSwCZCjSPByVYtMwUBjc2o
Microsoft Graph returns lowercase GUIDs for scopeOriginId in
AccessPackageResourceRoleScopes, but uppercase GUIDs for group IDs.
This caused JOIN failures in vw_UserPermissionAssignmentViaAccessPackage.

Fix applied in two places:
- Sync: normalize scopeOriginId to uppercase at sync time
- Views: use UPPER() in JOINs as safety net for existing data

https://claude.ai/code/session_01R9WuuiEFixJj7HCzP6dyse
Same Microsoft Graph lowercase GUID issue affects scopeId and roleId
fields in AccessPackageResourceRoleScopes. Normalize all three GUID
fields at sync time for consistency.

https://claude.ai/code/session_01R9WuuiEFixJj7HCzP6dyse
- Added resourceProvisioningOptions to default group attributes
- Added computed groupTypeCalculated field that classifies groups as:
  Unified Group with Team, Unified Group without Team,
  Distribution Group, Security Group, or Mail Enabled Security Group
- Ensures required attributes are always fetched from Graph even with
  custom attribute sets

https://claude.ai/code/session_01R9WuuiEFixJj7HCzP6dyse
…-uyh0M

Claude/add sql data visualization uyh0 m
…s, add automation step

- Add AuditLog.Read.All Graph API permission to fix user sync failure
- Change PIM (Group Eligible Members) sync default from No to Yes
- Add New-FGAzureAutomationAccount as step 4 in Next steps output

https://claude.ai/code/session_01UT5CbdJGD9kxhyfVziTpTD
When selecting an existing SQL Server, prompt for the existing password
instead of generating a new one. Moves server selection before credential
prompts so the context is correct.

https://claude.ai/code/session_01UT5CbdJGD9kxhyfVziTpTD
Add -ErrorAction Stop and try/catch around New-AzAutomationAccount.
Previously a creation error was non-terminating, causing the function
to continue and fail on every subsequent operation (variables, modules)
with cascading "resource not found" errors.

https://claude.ai/code/session_01UT5CbdJGD9kxhyfVziTpTD
When an existing token belongs to a different app registration than
the one specified in the config file, force re-authentication instead
of reusing the stale token. This prevents permission errors when
switching between app registrations.

https://claude.ai/code/session_01UT5CbdJGD9kxhyfVziTpTD
Replace the stale token detection logic with a simple fresh token
request on every sync. Takes < 1 second and eliminates all possible
stale/wrong token issues.

https://claude.ai/code/session_01UT5CbdJGD9kxhyfVziTpTD
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants