Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
cacc642
feat(exchange): add OOO calendar options (block calendar, decline inv…
kris6673 Mar 16, 2026
aac2f4e
refactor: replace bulk Graph request with per-method foreach loop
kris6673 Mar 19, 2026
951d2db
Dev to hf (#1939)
KelvinTegelaar Mar 19, 2026
5ecdc29
fix: text replacement for when tenant filter is unspecified
JohnDuprey Mar 20, 2026
94c0157
feat: Enhance security and functionality across multiple modules
JohnDuprey Mar 20, 2026
db380d0
Fix: Silly issue with removing legacy addins
Zacgoose Mar 20, 2026
1fc8a50
Fix: Silly issue with removing legacy addins (#1943)
KelvinTegelaar Mar 20, 2026
e5da743
Update Add-CIPPW32ScriptApplication.ps1
TecharyJames Mar 20, 2026
4f4eb48
fix: Optimize tenant processing by pre-expanding tenant groups in aud…
JohnDuprey Mar 21, 2026
2ef12d9
fix(groups): sanitize mailNickname for security group creation
Mar 21, 2026
963a98e
fix(group-templates): add validation for username and groupType
Mar 21, 2026
b25e385
feat(security): add MDE onboarding status report with caching
Mar 22, 2026
121a2cb
pr
Zacgoose Mar 22, 2026
0e4d015
Revert "pr"
Zacgoose Mar 22, 2026
88d4002
Refactor: MFA method removal to individual requests (#1938)
KelvinTegelaar Mar 22, 2026
ba8713b
feat: add MDE onboarding status report with caching (#1949)
KelvinTegelaar Mar 22, 2026
698e11c
Feat/variables in intune custom application (#1944)
KelvinTegelaar Mar 22, 2026
60e57dd
cleanup unnecessary checks
Mar 23, 2026
11c6bc0
fix: cleanup of standard template when removed
JohnDuprey Mar 23, 2026
c396867
fix: update inclusion/exclusion logic for tenant alignment
JohnDuprey Mar 23, 2026
cecaff6
fix: add initialDomainName support to logs and exo request
JohnDuprey Mar 23, 2026
c85052a
Add cmdlets to remove extension API keys
Zacgoose Mar 24, 2026
1eb4862
Add standardized webhook schema support
Zacgoose Mar 24, 2026
1d546e1
fix: Check extension standard
JohnDuprey Mar 24, 2026
152153e
feat: Check browser extension improvements
JohnDuprey Mar 24, 2026
a39e57f
fix: update extension name
JohnDuprey Mar 24, 2026
9916990
feat: Add OOO calendar options for Exchange (#1911)
JohnDuprey Mar 24, 2026
71468ca
fix: optimize role member retrieval in Invoke-ListRoles function
JohnDuprey Mar 24, 2026
8dc3091
Merge branch 'dev' of https://github.com/KelvinTegelaar/CIPP-API into…
JohnDuprey Mar 24, 2026
ab9cb1d
Add litigation/retention mailbox fields
Zacgoose Mar 25, 2026
452b987
Add webhook auth and tenant filter support
Zacgoose Mar 25, 2026
083040d
Add username space handling to user defaults
Zacgoose Mar 25, 2026
a48bc46
Add cmdlets to remove extension API keys (#1956)
KelvinTegelaar Mar 25, 2026
3b48260
Add username space handling to user defaults (#1959)
KelvinTegelaar Mar 25, 2026
f0a1227
Add litigation/retention mailbox fields (#1958)
KelvinTegelaar Mar 25, 2026
f47fb05
Fix: sanitize mailnickname (#1948)
KelvinTegelaar Mar 25, 2026
e73af4d
Feat: Add standardized webhook schema support and authentication meth…
KelvinTegelaar Mar 25, 2026
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
78 changes: 78 additions & 0 deletions Modules/CIPPCore/Private/Test-CIPPConditionFilter.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
function Test-CIPPConditionFilter {
<#
.SYNOPSIS
Returns a sanitized PowerShell condition string for an audit log / delta query condition.
.DESCRIPTION
Validates operator and property name against allowlists, sanitizes input values,
then returns a safe condition string suitable for [ScriptBlock]::Create().

This replaces the old Invoke-Expression pattern which was vulnerable to code injection
through unsanitized user-controlled fields.
.PARAMETER Condition
A single condition object with Property.label, Operator.value, and Input.value.
.OUTPUTS
[string] A sanitized PowerShell condition string, or $null if validation fails.
.FUNCTIONALITY
Internal
#>
[CmdletBinding()]
[OutputType([string])]
param(
[Parameter(Mandatory = $true)]
$Condition
)

# Operator allowlist - only these PowerShell comparison operators are permitted
$AllowedOperators = @(
'eq', 'ne', 'like', 'notlike', 'match', 'notmatch',
'gt', 'lt', 'ge', 'le', 'in', 'notin',
'contains', 'notcontains'
)

# Property name validation - only alphanumeric, underscores, and dots allowed
$SafePropertyRegex = [regex]'^[a-zA-Z0-9_.]+$'

# Value sanitization - block characters that enable code injection
$UnsafeValueRegex = [regex]'[;|`\$\{\}]'

$propertyName = $Condition.Property.label
$operatorValue = $Condition.Operator.value.ToLower()
$inputValue = $Condition.Input.value

# Validate operator against allowlist
if ($operatorValue -notin $AllowedOperators) {
Write-Warning "Blocked invalid operator '$($Condition.Operator.value)' in condition for property '$propertyName'"
return $null
}

# Validate property name to prevent injection via property paths
if (-not $SafePropertyRegex.IsMatch($propertyName)) {
Write-Warning "Blocked invalid property name '$propertyName' in condition"
return $null
}

# Build sanitized condition string
if ($inputValue -is [array]) {
# Sanitize each array element
$sanitizedItems = foreach ($item in $inputValue) {
$itemStr = [string]$item
if ($UnsafeValueRegex.IsMatch($itemStr)) {
Write-Warning "Blocked unsafe value in array for property '$propertyName': '$itemStr'"
return $null
}
$itemStr -replace "'", "''"
}
if ($null -eq $sanitizedItems) { return $null }
$arrayAsString = $sanitizedItems | ForEach-Object { "'$_'" }
$value = "@($($arrayAsString -join ', '))"
} else {
$valueStr = [string]$inputValue
if ($UnsafeValueRegex.IsMatch($valueStr)) {
Write-Warning "Blocked unsafe value for property '$propertyName': '$valueStr'"
return $null
}
$value = "'$($valueStr -replace "'", "''")'"
}

return "`$(`$_.$propertyName) -$operatorValue $value"
}
210 changes: 210 additions & 0 deletions Modules/CIPPCore/Private/Test-CIPPDynamicGroupFilter.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
function Test-CIPPDynamicGroupFilter {
<#
.SYNOPSIS
Returns a sanitized PowerShell condition string for a dynamic tenant group rule.
.DESCRIPTION
Validates all user-controlled inputs (property, operator, values) against allowlists
and sanitizes values before building the condition string. Returns a safe condition
string suitable for use in [ScriptBlock]::Create().

This replaces the old pattern of directly interpolating unsanitized user input into
scriptblock strings, which was vulnerable to code injection.
.PARAMETER Rule
A single rule object with .property, .operator, and .value fields.
.PARAMETER TenantGroupMembersCache
Hashtable of group memberships keyed by group ID.
.OUTPUTS
[string] A sanitized PowerShell condition string, or $null if validation fails.
.FUNCTIONALITY
Internal
#>
[CmdletBinding()]
[OutputType([string])]
param(
[Parameter(Mandatory = $true)]
$Rule,
[Parameter(Mandatory = $false)]
[hashtable]$TenantGroupMembersCache = @{}
)

$AllowedOperators = @('eq', 'ne', 'like', 'notlike', 'in', 'notin', 'contains', 'notcontains')
$AllowedProperties = @('delegatedAccessStatus', 'availableLicense', 'availableServicePlan', 'tenantGroupMember', 'customVariable')

# Regex for sanitizing string values - block characters that enable code injection
$SafeValueRegex = [regex]'^[^;|`\$\{\}\(\)]*$'
# Regex for GUID validation
$GuidRegex = [regex]'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
# Regex for safe identifiers (variable names, plan names, etc.)
$SafeIdentifierRegex = [regex]'^[a-zA-Z0-9_.\-\s\(\)]+$'

$Property = $Rule.property
$Operator = [string]($Rule.operator)
$OperatorLower = $Operator.ToLower()
$Value = $Rule.value

# Validate operator
if ($OperatorLower -notin $AllowedOperators) {
Write-Warning "Blocked invalid operator '$Operator' in dynamic group rule for property '$Property'"
return $null
}

# Validate property
if ($Property -notin $AllowedProperties) {
Write-Warning "Blocked invalid property '$Property' in dynamic group rule"
return $null
}

# Helper: sanitize a single string value for safe embedding in a quoted string
function Protect-StringValue {
param([string]$InputValue)
# Escape single quotes by doubling them (PowerShell string escaping)
$escaped = $InputValue -replace "'", "''"
# Block any remaining injection characters
if (-not $SafeValueRegex.IsMatch($escaped)) {
Write-Warning "Blocked unsafe value: '$InputValue'"
return $null
}
return $escaped
}

# Helper: sanitize and format an array of string values for embedding in @('a','b')
function Protect-StringArray {
param([array]$InputValues)
$sanitized = foreach ($v in $InputValues) {
$clean = Protect-StringValue -InputValue ([string]$v)
if ($null -eq $clean) { return $null }
"'$clean'"
}
return "@($($sanitized -join ', '))"
}

switch ($Property) {
'delegatedAccessStatus' {
$safeValue = Protect-StringValue -InputValue ([string]$Value.value)
if ($null -eq $safeValue) { return $null }
return "`$_.delegatedPrivilegeStatus -$OperatorLower '$safeValue'"
}
'availableLicense' {
if ($OperatorLower -in @('in', 'notin')) {
$arrayValues = @(if ($Value -is [array]) { $Value.guid } else { @($Value.guid) })
# Validate each GUID
foreach ($g in $arrayValues) {
if (![string]::IsNullOrEmpty($g) -and -not $GuidRegex.IsMatch($g)) {
Write-Warning "Blocked invalid GUID in availableLicense rule: '$g'"
return $null
}
}
$arrayAsString = ($arrayValues | Where-Object { ![string]::IsNullOrEmpty($_) }) | ForEach-Object { "'$_'" }
if ($OperatorLower -eq 'in') {
return "(`$_.skuId | Where-Object { `$_ -in @($($arrayAsString -join ', ')) }).Count -gt 0"
} else {
return "(`$_.skuId | Where-Object { `$_ -in @($($arrayAsString -join ', ')) }).Count -eq 0"
}
} else {
$guid = [string]$Value.guid
if (![string]::IsNullOrEmpty($guid) -and -not $GuidRegex.IsMatch($guid)) {
Write-Warning "Blocked invalid GUID in availableLicense rule: '$guid'"
return $null
}
return "`$_.skuId -$OperatorLower '$guid'"
}
}
'availableServicePlan' {
if ($OperatorLower -in @('in', 'notin')) {
$arrayValues = @(if ($Value -is [array]) { $Value.value } else { @($Value.value) })
foreach ($v in $arrayValues) {
if (![string]::IsNullOrEmpty($v) -and -not $SafeIdentifierRegex.IsMatch($v)) {
Write-Warning "Blocked invalid service plan name: '$v'"
return $null
}
}
$arrayAsString = ($arrayValues | Where-Object { ![string]::IsNullOrEmpty($_) }) | ForEach-Object { "'$_'" }
if ($OperatorLower -eq 'in') {
return "(`$_.servicePlans | Where-Object { `$_ -in @($($arrayAsString -join ', ')) }).Count -gt 0"
} else {
return "(`$_.servicePlans | Where-Object { `$_ -in @($($arrayAsString -join ', ')) }).Count -eq 0"
}
} else {
$safeValue = Protect-StringValue -InputValue ([string]$Value.value)
if ($null -eq $safeValue) { return $null }
return "`$_.servicePlans -$OperatorLower '$safeValue'"
}
}
'tenantGroupMember' {
if ($OperatorLower -in @('in', 'notin')) {
$ReferencedGroupIds = @($Value.value)
# Validate group IDs are GUIDs
foreach ($gid in $ReferencedGroupIds) {
if (![string]::IsNullOrEmpty($gid) -and -not $GuidRegex.IsMatch($gid)) {
Write-Warning "Blocked invalid group ID in tenantGroupMember rule: '$gid'"
return $null
}
}

$AllMembers = [System.Collections.Generic.HashSet[string]]::new()
foreach ($GroupId in $ReferencedGroupIds) {
if ($TenantGroupMembersCache.ContainsKey($GroupId)) {
foreach ($MemberId in $TenantGroupMembersCache[$GroupId]) {
[void]$AllMembers.Add($MemberId)
}
}
}

$MemberArray = $AllMembers | ForEach-Object { "'$_'" }
$MemberArrayString = $MemberArray -join ', '

if ($OperatorLower -eq 'in') {
return "`$_.customerId -in @($MemberArrayString)"
} else {
return "`$_.customerId -notin @($MemberArrayString)"
}
} else {
$ReferencedGroupId = [string]$Value.value
if (![string]::IsNullOrEmpty($ReferencedGroupId) -and -not $GuidRegex.IsMatch($ReferencedGroupId)) {
Write-Warning "Blocked invalid group ID: '$ReferencedGroupId'"
return $null
}
return "`$_.customerId -$OperatorLower `$script:TenantGroupMembersCache['$ReferencedGroupId']"
}
}
'customVariable' {
$VariableName = if ($Value.variableName -is [string]) {
$Value.variableName
} elseif ($Value.variableName.value) {
$Value.variableName.value
} else {
[string]$Value.variableName
}
# Validate variable name - alphanumeric, underscores, hyphens, dots only
if (-not $SafeIdentifierRegex.IsMatch($VariableName)) {
Write-Warning "Blocked invalid custom variable name: '$VariableName'"
return $null
}
$ExpectedValue = Protect-StringValue -InputValue ([string]$Value.value)
if ($null -eq $ExpectedValue) { return $null }

switch ($OperatorLower) {
'eq' {
return "(`$_.customVariables.ContainsKey('$VariableName') -and `$_.customVariables['$VariableName'].Value -eq '$ExpectedValue')"
}
'ne' {
return "(-not `$_.customVariables.ContainsKey('$VariableName') -or `$_.customVariables['$VariableName'].Value -ne '$ExpectedValue')"
}
'like' {
return "(`$_.customVariables.ContainsKey('$VariableName') -and `$_.customVariables['$VariableName'].Value -like '*$ExpectedValue*')"
}
'notlike' {
return "(-not `$_.customVariables.ContainsKey('$VariableName') -or `$_.customVariables['$VariableName'].Value -notlike '*$ExpectedValue*')"
}
default {
Write-Warning "Unsupported operator '$OperatorLower' for customVariable"
return $null
}
}
}
default {
Write-Warning "Unknown property type: $Property"
return $null
}
}
}
28 changes: 21 additions & 7 deletions Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ function Add-CIPPW32ScriptApplication {
- publisher: Publisher name
- installScript (required): PowerShell install script content (plaintext)
- uninstallScript: PowerShell uninstall script content (plaintext)
- detectionPath (required): Full path to the file or folder to detect (e.g., 'C:\\Program Files\\MyApp')
- detectionScript: PowerShell detection script content (plaintext). Takes priority over file detection.
Script should write output to STDOUT and exit 0 when app is detected (installed).
- detectionPath: Full path to the file or folder to detect (e.g., 'C:\\Program Files\\MyApp')
- detectionFile: File name to detect (optional, for folder path detection)
- detectionType: 'exists', 'modifiedDate', 'createdDate', 'version', 'sizeInMB' (default: 'exists')
- check32BitOn64System: Boolean, check 32-bit registry/paths on 64-bit systems (default: false)
Expand Down Expand Up @@ -69,9 +71,20 @@ function Add-CIPPW32ScriptApplication {
$FileName = $ChocoXml.ApplicationInfo.FileName
$UnencryptedSize = [int64]$ChocoXml.ApplicationInfo.UnencryptedContentSize

# Build detection rules
if ($Properties.detectionPath) {
# Determine if this is a file or folder detection
# Build detection rules — detection script takes priority, then file detection, then marker file fallback
if ($Properties.detectionScript) {
# PowerShell script detection: script should write to STDOUT and exit 0 when detected
$DetectionScriptContent = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Properties.detectionScript))
$DetectionRules = @(
@{
'@odata.type' = '#microsoft.graph.win32LobAppPowerShellScriptDetection'
scriptContent = $DetectionScriptContent
enforceSignatureCheck = if ($null -ne $Properties.enforceSignatureCheck) { [bool]$Properties.enforceSignatureCheck } else { $false }
runAs32Bit = if ($null -ne $Properties.runAs32Bit) { [bool]$Properties.runAs32Bit } else { $false }
}
)
} elseif ($Properties.detectionPath) {
# File system detection
$DetectionRule = @{
'@odata.type' = '#microsoft.graph.win32LobAppFileSystemDetection'
check32BitOn64System = if ($null -ne $Properties.check32BitOn64System) { [bool]$Properties.check32BitOn64System } else { $false }
Expand All @@ -84,7 +97,6 @@ function Add-CIPPW32ScriptApplication {
$DetectionRule['fileOrFolderName'] = $Properties.detectionFile
} else {
# Folder/File detection (full path)
# Split the path into directory and file/folder name
$PathItem = Split-Path $Properties.detectionPath -Leaf
$ParentPath = Split-Path $Properties.detectionPath -Parent

Expand Down Expand Up @@ -149,7 +161,8 @@ function Add-CIPPW32ScriptApplication {
$UninstallScriptId = $null

if ($Properties.installScript) {
$InstallScriptContent = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Properties.installScript))
$ReplacedInstallScript = Get-CIPPTextReplacement -Text $Properties.installScript -TenantFilter $TenantFilter
$InstallScriptContent = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($ReplacedInstallScript))
$InstallScriptBody = @{
'@odata.type' = '#microsoft.graph.win32LobAppInstallPowerShellScript'
displayName = 'install.ps1'
Expand All @@ -172,7 +185,8 @@ function Add-CIPPW32ScriptApplication {
}

if ($Properties.uninstallScript) {
$UninstallScriptContent = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Properties.uninstallScript))
$ReplacedUninstallScript = Get-CIPPTextReplacement -Text $Properties.uninstallScript -TenantFilter $TenantFilter
$UninstallScriptContent = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($ReplacedUninstallScript))
$UninstallScriptBody = @{
'@odata.type' = '#microsoft.graph.win32LobAppUninstallPowerShellScript'
displayName = 'uninstall.ps1'
Expand Down
Loading