Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
bec8e7a
add initial script to fetch provisioning config from entra ID
wojcik91 Oct 16, 2025
89ad243
update naming and return values
wojcik91 Oct 16, 2025
511880a
fetch provisioning config from AD extension attributes
wojcik91 Oct 16, 2025
008f67e
update docstring
wojcik91 Oct 16, 2025
3962c5b
read data from a single extension attribute
wojcik91 Oct 16, 2025
db00f98
pass extension attribute as argument
wojcik91 Oct 16, 2025
75d24ee
us an arbitrary AD attribute
wojcik91 Oct 17, 2025
40248d8
remove unnecessary escape chars
wojcik91 Oct 20, 2025
c282ad8
run script during MSI install
wojcik91 Oct 20, 2025
bfaa3b7
add MSI properties to control script execution
wojcik91 Oct 20, 2025
4faf3c9
Merge branch 'dev' into windows_provisioning_script_poc
wojcik91 Oct 21, 2025
a0ea6fd
test new MSI build
wojcik91 Oct 21, 2025
3e1b967
remove silent mode switch
wojcik91 Oct 23, 2025
57dd8fc
fix provisioning fragment
wojcik91 Oct 23, 2025
72d0c3b
restore upgrade code
wojcik91 Oct 23, 2025
94eb33a
add provisioning script logging
wojcik91 Oct 23, 2025
34297b8
rename provisioning config file
wojcik91 Oct 23, 2025
2331093
ensure transcript stop
wojcik91 Oct 23, 2025
18063de
Merge branch 'dev' into windows_provisioning_script_poc
wojcik91 Oct 23, 2025
77040af
don't restore legacy file
wojcik91 Oct 23, 2025
906ad81
simplify provisioning config file in PS script
wojcik91 Oct 23, 2025
50d52bc
restore script resources
wojcik91 Oct 23, 2025
cc5eff2
try to sidestep windows encoding issues
wojcik91 Oct 24, 2025
f33b520
strip BOM manually
wojcik91 Oct 24, 2025
576e04d
test new MSI images
wojcik91 Oct 24, 2025
258bf69
setup MSI banners
wojcik91 Oct 24, 2025
0198b43
move DB migrations execution
wojcik91 Oct 24, 2025
cc5b7fa
change default AD attribute
wojcik91 Oct 24, 2025
ec973e9
restore release workflow
wojcik91 Oct 24, 2025
29ee33a
Update src-tauri/resources-windows/scripts/Get-ProvisioningConfig.ps1
wojcik91 Oct 24, 2025
a603b68
restore EOF newlines
wojcik91 Oct 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src-tauri/resources-windows/fragments/provisioning.wxs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Fragment>
<!-- Component to include the PowerShell script -->
<Component Id="ProvisioningScriptFragment" Directory="INSTALLDIR" Guid="*">
<File Id="GetProvisioningConfigScript" Source="..\..\resources-windows\scripts\Get-ProvisioningConfig.ps1" KeyPath="yes"/>
</Component>

<!-- Define public properties that can be passed to the MSI -->
<Property Id="PROVISIONING" Secure="yes"/>
<Property Id="ADATTRIBUTE" Value="defguardProvisioningConfig"/>

<!-- Custom action to run the PowerShell script -->
<CustomAction Id="RunProvisioningScript"
Directory="INSTALLDIR"
Execute="deferred"
Impersonate="yes"
Return="check"
ExeCommand="powershell.exe -ExecutionPolicy Bypass -File &quot;[INSTALLDIR]Get-ProvisioningConfig.ps1&quot; -ADAttribute &quot;[ADATTRIBUTE]&quot;"/>

<!-- Schedule the custom action to run only if PROVISIONING property is set -->
<InstallExecuteSequence>
<Custom Action="RunProvisioningScript" After="InstallFiles">PROVISIONING AND NOT REMOVE</Custom>
</InstallExecuteSequence>
</Fragment>
</Wix>
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
<Fragment>
<DirectoryRef Id="INSTALLDIR">
<Component Id="DefGuardServiceFragment">
<Component Id="DefguardServiceFragment">
<File KeyPath="yes" Id="DefguardServiceFile" Source="..\..\defguard-service.exe" />
<ServiceInstall
Account="LocalSystem"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src-tauri/resources-windows/msi/top_banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
316 changes: 316 additions & 0 deletions src-tauri/resources-windows/scripts/Get-ProvisioningConfig.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
#Requires -Version 5.1

<#
.SYNOPSIS
Retrieves Defguard client provisioning configuration for the currently logged-in user from Active Directory or Entra ID.

.DESCRIPTION
This script detects whether the computer is joined to on-premises Active Directory
or Entra ID (Azure AD), then fetches Defguard provisioning data (URL and enrollment token) from the appropriate source.
- On-premises AD: Reads from specified attribute (default: defguardProvisioningConfig)
- Entra ID: Reads from custom security attributes under the 'Defguard' set
- Workgroup: Exits gracefully
The retrieved enrollment data is saved to a JSON file for the Defguard client to use.

.PARAMETER ADAttribute
Specifies which Active Directory attribute to read from (default: defguardProvisioningConfig)
#>

param(
[string]$ADAttribute = "defguardProvisioningConfig"
)

# Check device join status
function Get-DomainJoinStatus {
try {
$computerSystem = Get-WmiObject -Class Win32_ComputerSystem -ErrorAction Stop

# Check for traditional domain join
if ($computerSystem.PartOfDomain -eq $true) {
return @{
JoinType = "OnPremisesAD"
Domain = $computerSystem.Domain
}
}

# Check for Entra ID (Azure AD) join
$dsregStatus = dsregcmd /status
if ($dsregStatus -match "AzureAdJoined\s*:\s*YES") {
$tenantName = ($dsregStatus | Select-String "TenantName\s*:\s*(.+)").Matches.Groups[1].Value.Trim()
return @{
JoinType = "EntraID"
Domain = $tenantName
}
}

# Check for Hybrid join
if ($dsregStatus -match "DomainJoined\s*:\s*YES" -and $dsregStatus -match "AzureAdJoined\s*:\s*YES") {
return @{
JoinType = "Hybrid"
Domain = $computerSystem.Domain
}
}

# Not joined to any directory
return @{
JoinType = "Workgroup"
Domain = $null
}

} catch {
Write-Host "Unable to determine domain status: $_" -ForegroundColor Yellow
return @{
JoinType = "Unknown"
Domain = $null
}
}
}

# Save Defguard enrollment data to JSON
function Save-DefguardEnrollmentData {
param(
[string]$EnrollmentUrl,
[string]$EnrollmentToken
)

# Create Defguard directory in AppData\Roaming
$defguardDir = Join-Path $env:APPDATA "net.defguard"
$jsonOutputPath = Join-Path $defguardDir "provisioning.json"

try {
# Create directory if it doesn't exist
if (-not (Test-Path -Path $defguardDir)) {
New-Item -ItemType Directory -Path $defguardDir -Force | Out-Null
Write-Host "`nCreated directory: $defguardDir" -ForegroundColor Gray
}

$jsonData = @{
enrollment_url = $EnrollmentUrl
enrollment_token = $EnrollmentToken
}

$jsonData | ConvertTo-Json -Depth 10 | Out-File -FilePath $jsonOutputPath -Encoding UTF8 -Force
Write-Host "`nDefguard enrollment data saved to: $jsonOutputPath" -ForegroundColor Green
return $true
} catch {
Write-Host "`nFailed to save JSON file: $_" -ForegroundColor Red
return $false
}
}

# Get Defguard client provisioning config from on-premises AD
function Get-OnPremisesADProvisioningConfig {
param(
[string]$Username,
[string]$ADAttribute
)

# Check if Active Directory module is available
if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) {
Write-Host "Active Directory module is not installed. Please install RSAT tools." -ForegroundColor Red
return
}

# Import the Active Directory module
try {
Import-Module ActiveDirectory -ErrorAction Stop
} catch {
Write-Host "Failed to import Active Directory module: $_" -ForegroundColor Red
return
}

# Fetch AD user information
try {
$adUser = Get-ADUser -Identity $Username -Properties * -ErrorAction Stop

# Display user information
Write-Host "`n=== On-Premises Active Directory User Information ===" -ForegroundColor Cyan
Write-Host "Display Name: $($adUser.DisplayName)"
Write-Host "Username (SAM): $($adUser.SamAccountName)"
Write-Host "User Principal Name: $($adUser.UserPrincipalName)"
Write-Host "Email: $($adUser.EmailAddress)"
Write-Host "Enabled: $($adUser.Enabled)"
Write-Host "Created: $($adUser.Created)"
Write-Host "Distinguished Name: $($adUser.DistinguishedName)"
Write-Host "======================================================`n" -ForegroundColor Cyan

# Check for Defguard enrollment data in the specified AD attribute
Write-Host "`n--- Active Directory Attribute ---" -ForegroundColor Yellow

# Read JSON data from the specified AD attribute
$jsonData = $adUser.$ADAttribute

Write-Host "Defguard Enrollment JSON ($ADAttribute): $jsonData"

if ($jsonData) {
try {
# Parse the JSON data
$enrollmentConfig = $jsonData | ConvertFrom-Json -ErrorAction Stop

# Extract URL and token from the parsed JSON
$enrollmentUrl = $enrollmentConfig.enrollmentUrl
$enrollmentToken = $enrollmentConfig.enrollmentToken

Write-Host "Defguard Enrollment URL: $enrollmentUrl"
Write-Host "Defguard Enrollment Token: $enrollmentToken"

# Save enrollment data to JSON file only if both URL and token exist
if ($enrollmentUrl -and $enrollmentToken) {
Save-DefguardEnrollmentData -EnrollmentUrl $enrollmentUrl `
-EnrollmentToken $enrollmentToken
} else {
Write-Host "`nWarning: Incomplete Defguard enrollment data in JSON. Both URL and token are required." -ForegroundColor Yellow
}
} catch {
Write-Host "Failed to parse JSON from AD attribute '$ADAttribute': $_" -ForegroundColor Red
Write-Host "JSON data should be in format: {`"enrollmentUrl`":`"https://...`",`"enrollmentToken`":`"token-value`"}" -ForegroundColor Yellow
}
} else {
Write-Host "No Defguard enrollment data found in the specified AD attribute." -ForegroundColor Yellow
}

Write-Host "======================================================`n" -ForegroundColor Cyan


return

} catch {
Write-Host "Failed to retrieve AD user information for '$Username': $_" -ForegroundColor Red
return
}
}

# Get Defguard client provisioning config from Entra ID
function Get-EntraIDProvisioningConfig {
# Check if Microsoft.Graph module is available
if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Users)) {
Write-Host "Microsoft.Graph.Users module is not installed." -ForegroundColor Yellow
Write-Host "Install it with: Install-Module Microsoft.Graph.Users -Scope CurrentUser" -ForegroundColor Yellow
return
}

# Import the module
try {
Import-Module Microsoft.Graph.Users -ErrorAction Stop
} catch {
Write-Host "Failed to import Microsoft.Graph.Users module: $_" -ForegroundColor Red
return
}

# Connect to Microsoft Graph
try {
$context = Get-MgContext -ErrorAction SilentlyContinue

if (-not $context) {
Write-Host "Connecting to Microsoft Graph (authentication required)..." -ForegroundColor Yellow
Write-Host "Note: Requesting additional permissions for custom security attributes..." -ForegroundColor Gray
Connect-MgGraph -Scopes "User.Read", "CustomSecAttributeAssignment.Read.All" -ErrorAction Stop
} else {
# Check if we have the required scope for custom attributes
$hasCustomAttrScope = $context.Scopes -contains "CustomSecAttributeAssignment.Read.All"
if (-not $hasCustomAttrScope) {
Write-Host "Warning: Missing 'CustomSecAttributeAssignment.Read.All' permission." -ForegroundColor Yellow
Write-Host "Custom security attributes will not be available. Reconnect with:" -ForegroundColor Yellow
Write-Host " Connect-MgGraph -Scopes 'User.Read', 'CustomSecAttributeAssignment.Read.All'" -ForegroundColor Gray
return
}
}

# Get current user info including custom security attributes
$properties = @(
"DisplayName",
"UserPrincipalName",
"Mail",
"AccountEnabled",
"CreatedDateTime",
"Id",
"CustomSecurityAttributes"
)

$mgUser = Get-MgUser -UserId (Get-MgContext).Account -Property $properties -ErrorAction Stop

# Display user information
Write-Host "`n=== Entra ID (Azure AD) User Information ===" -ForegroundColor Cyan
Write-Host "Display Name: $($mgUser.DisplayName)"
Write-Host "User Principal Name: $($mgUser.UserPrincipalName)"
Write-Host "Email: $($mgUser.Mail)"
Write-Host "Account Enabled: $($mgUser.AccountEnabled)"
Write-Host "Created: $($mgUser.CreatedDateTime)"
Write-Host "User ID: $($mgUser.Id)"

# Try to get custom security attributes
if ($mgUser.CustomSecurityAttributes) {
Write-Host "`n--- Custom Security Attributes ---" -ForegroundColor Yellow

# Access Defguard attributes
if ($mgUser.CustomSecurityAttributes.AdditionalProperties) {
$defguardAttrs = $mgUser.CustomSecurityAttributes.AdditionalProperties["Defguard"]

if ($defguardAttrs) {
$enrollmentUrl = $defguardAttrs["EnrollmentUrl"]
$enrollmentToken = $defguardAttrs["EnrollmentToken"]

Write-Host "Defguard Enrollment URL: $enrollmentUrl"
Write-Host "Defguard Enrollment Token: $enrollmentToken"

# Save enrollment data to JSON file only if both URL and token exist
if ($enrollmentUrl -and $enrollmentToken) {
Save-DefguardEnrollmentData -EnrollmentUrl $enrollmentUrl `
-EnrollmentToken $enrollmentToken
} else {
Write-Host "`nWarning: Incomplete Defguard enrollment data. Both URL and token are required." -ForegroundColor Yellow
}
} else {
Write-Host "No Defguard attributes found for this user." -ForegroundColor Gray
}
} else {
Write-Host "No custom security attributes found." -ForegroundColor Gray
}
} else {
Write-Host "`nCustom security attributes not available." -ForegroundColor Gray
Write-Host "(May require additional permissions or attributes not set)" -ForegroundColor Gray
}

Write-Host "=============================================`n" -ForegroundColor Cyan

} catch {
Write-Host "Failed to retrieve Entra ID user information: $_" -ForegroundColor Red
Write-Host "Error details: $($_.Exception.Message)" -ForegroundColor Red
}
}

# Log all script output to file
$defguardDir = Join-Path $env:APPDATA "net.defguard"
$logFilePath = Join-Path $defguardDir "provisioning_log.txt"
Start-Transcript -Path $logFilePath

# Main script execution
Write-Host "Detecting domain join status..." -ForegroundColor Gray

$joinStatus = Get-DomainJoinStatus
$joinType = $joinStatus.JoinType

Write-Host "Join Type = '$joinType'" -ForegroundColor Magenta

if ($joinType -eq "OnPremisesAD") {
Write-Host "Connected to on-premises Active Directory: $($joinStatus.Domain)" -ForegroundColor Green
$currentUser = $env:USERNAME
Get-OnPremisesADProvisioningConfig -Username $currentUser -ADAttribute $ADAttribute
} elseif ($joinType -eq "Hybrid") {
Write-Host "Hybrid join detected (both on-premises AD and Entra ID): $($joinStatus.Domain)" -ForegroundColor Green
Write-Host "Querying on-premises Active Directory..." -ForegroundColor Gray
$currentUser = $env:USERNAME
Get-OnPremisesADProvisioningConfig -Username $currentUser -ADAttribute $ADAttribute
} elseif ($joinType -eq "EntraID") {
Write-Host "Connected to Entra ID (Azure AD)" -ForegroundColor Green
if ($joinStatus.Domain) {
Write-Host " Tenant: $($joinStatus.Domain)" -ForegroundColor Gray
}
Get-EntraIDProvisioningConfig
} elseif ($joinType -eq "Workgroup") {
Write-Host "This computer is not connected to a domain (Workgroup). Exiting." -ForegroundColor Yellow
} else {
Write-Host "Unable to determine domain connection status. Exiting." -ForegroundColor Yellow
}

Stop-Transcript
12 changes: 4 additions & 8 deletions src-tauri/src/bin/defguard-client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use defguard_client::{
appstate::AppState,
commands::*,
database::{
handle_db_migrations,
models::{location_stats::LocationStats, tunnel::TunnelStats},
DB_POOL,
},
Expand All @@ -39,14 +40,6 @@ const LOGGING_TARGET_IGNORE_LIST: [&str; 5] = ["tauri", "sqlx", "hyper", "h2", "
static LOG_INCLUDES: LazyLock<Vec<String>> = LazyLock::new(load_log_targets);

async fn startup(app_handle: &AppHandle) {
debug!("Running database migrations, if there are any.");
sqlx::migrate!()
.run(&*DB_POOL)
.await
.expect("Failed to apply database migrations.");
debug!("Applied all database migrations that were pending. If any.");
debug!("Database setup has been completed successfully.");

debug!("Purging old stats from the database.");
if let Err(err) = LocationStats::purge(&*DB_POOL).await {
error!("Failed to purge location stats: {err}");
Expand Down Expand Up @@ -246,6 +239,9 @@ fn main() {
.build(),
)?;

// run DB migrations
tauri::async_runtime::block_on(handle_db_migrations());

// Check if client needs to be initialized
// and try to load provisioning config if necessary
let provisioning_config =
Expand Down
10 changes: 10 additions & 0 deletions src-tauri/src/database/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,13 @@ fn prepare_db_url() -> Result<String, Error> {
))
}
}

pub async fn handle_db_migrations() {
debug!("Running database migrations, if there are any.");
sqlx::migrate!()
.run(&*DB_POOL)
.await
.expect("Failed to apply database migrations.");
debug!("Applied all database migrations that were pending. If any.");
debug!("Database setup has been completed successfully.");
}
Loading