Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b2985a6
fix(assignments): change default mode to append for consistency
kris6673 Jul 13, 2026
5b42363
feat(nudge-mfa): add group targeting and auth method selection
TecharyJames Jul 14, 2026
2a86601
Feat: authenticator campaign improvments (#2139)
KelvinTegelaar Jul 14, 2026
a557f51
fix: Change default assignment mode to append (#2137)
KelvinTegelaar Jul 14, 2026
24db2c5
feat(ninjaone): auto-create CVE vulnerability scan group when missing
MWG-Logan Jul 14, 2026
2bc0c8e
fix(standards): cast retention days to integer
Zacgoose Jul 15, 2026
986de02
chore: align frontend file with api file
Zacgoose Jul 15, 2026
8f1dd76
fix(standards): normalize smart lockout mode
Zacgoose Jul 15, 2026
25f38c5
fix(cisa): correct EXO 1.4.1 failure output
Zacgoose Jul 15, 2026
2856852
fix(orca): validate ZAP-supported spam actions
Zacgoose Jul 15, 2026
45bd6c9
feat(cache): implement field projection for test data
Zacgoose Jul 15, 2026
ba8232e
fix(cippdb): cache PIM role data with app token
Zacgoose Jul 15, 2026
98f0778
fix: correct field references in identity and device tests
Zacgoose Jul 15, 2026
003e39f
fix: restore all name/displayName pairings in drift report Intune mat…
MWG-Logan Jul 15, 2026
e247438
feat(config): add copilot policy permissions
Zacgoose Jul 16, 2026
8cde47b
Skip SharePointSharingLinks in DB cache run
Zacgoose Jul 16, 2026
02b0582
fix: restore all name/displayName pairings in drift report Intune mat…
KelvinTegelaar Jul 16, 2026
fb4dec1
feat(ninjaone): auto-create CVE vulnerability scan group when missing…
KelvinTegelaar Jul 16, 2026
01242ff
fix(user): route group updates by effective type
Zacgoose Jul 16, 2026
bc7ace6
Update version_latest.txt
KelvinTegelaar Jul 16, 2026
3529092
Dev to hf (#2144)
KelvinTegelaar Jul 16, 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
18 changes: 18 additions & 0 deletions Config/PermissionsTranslator.json
Original file line number Diff line number Diff line change
Expand Up @@ -5371,5 +5371,23 @@
"userConsentDescription": "Allows the app to read your tenant's acquired telephone number details, without a signed-in user. Acquired telephone numbers may include attributes related to assigned object, emergency location, network site, etc.",
"userConsentDisplayName": "Allows the app to read your tenant's acquired telephone number details, without a signed-in user. Acquired telephone numbers may include attributes related to assigned object, emergency location, network site, etc.",
"value": "TeamsTelephoneNumber.ReadWrite.All"
},
{
"description": "Allows the app to read and write Copilot policy settings for the organization, on behalf of the signed-in user.",
"displayName": "Read and write Copilot policy settings",
"id": "e2edbde8-4448-4e49-8ebb-d53ba72df0f3",
"Origin": "Delegated",
"userConsentDescription": "Allows the app to read and write Copilot policy settings for the organization, on your behalf.",
"userConsentDisplayName": "Read and write Copilot policy settings",
"value": "CopilotPolicySettings.ReadWrite"
},
{
"description": "Allows the app to read and write Copilot policy settings for the organization, without a signed-in user.",
"displayName": "Read and write Copilot policy settings",
"id": "cc147c17-b8e8-4d3f-9f94-aa9e279a079a",
"Origin": "Application",
"userConsentDescription": "Allows the app to read and write Copilot policy settings for the organization, without a signed-in user.",
"userConsentDisplayName": "Read and write Copilot policy settings",
"value": "CopilotPolicySettings.ReadWrite"
}
]
38 changes: 37 additions & 1 deletion Config/SAMManifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,42 @@
{
"id": "b7887744-6746-4312-813d-72daeaee7e2d",
"type": "Scope"
},
{
"id": "dd199f4a-f148-40a4-a2ec-f0069cc799ec",
"type": "Role"
},
{
"id": "8c026be3-8e26-4774-9372-8d5d6f21daff",
"type": "Scope"
},
{
"id": "fee28b28-e1f3-4841-818e-2704dc62245f",
"type": "Role"
},
{
"id": "62ade113-f8e0-4bf9-a6ba-5acb31db32fd",
"type": "Scope"
},
{
"id": "9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8",
"type": "Role"
},
{
"id": "31e08e0a-d3f7-4ca2-ac39-7343fb83e8ad",
"type": "Role"
},
{
"id": "1ff1be21-34eb-448c-9ac9-ce1f506b2a68",
"type": "Scope"
},
{
"id": "cc147c17-b8e8-4d3f-9f94-aa9e279a079a",
"type": "Role"
},
{
"id": "e2edbde8-4448-4e49-8ebb-d53ba72df0f3",
"type": "Scope"
}
]
},
Expand Down Expand Up @@ -689,4 +725,4 @@
"isEnabled": true,
"allProperties": true
}
}
}
63 changes: 57 additions & 6 deletions Config/standards.json
Original file line number Diff line number Diff line change
Expand Up @@ -1371,21 +1371,38 @@
"cat": "Entra (AAD) Standards",
"tag": ["SMB1001 (2.5)"],
"appliesToTest": ["SMB1001_2_5", "ZTNA21889"],
"helpText": "Sets the state of the registration campaign for the tenant",
"docsDescription": "Sets the state of the registration campaign for the tenant. If enabled nudges users to set up the Microsoft Authenticator during sign-in.",
"helpText": "Sets the state of the registration campaign for the tenant, including the targeted authentication method, snooze settings and include/exclude groups. Leave include/exclude blank to keep the groups currently configured in the tenant, or use 'AllUsers' to target all users.",
"docsDescription": "Sets the state of the registration campaign for the tenant. If enabled nudges users to set up the targeted authentication method (Microsoft Authenticator or a Passkey) during sign-in. Supports limiting the number of snoozes, and including or excluding specific groups (by display name).",
"executiveText": "Prompts employees to set up multi-factor authentication during login, gradually improving the organization's security posture by encouraging adoption of stronger authentication methods. This helps achieve better security compliance without forcing immediate mandatory changes.",
"addedComponent": [
{
"type": "autoComplete",
"multiple": false,
"creatable": false,
"label": "Select value",
"label": "Registration campaign state",
"name": "standards.NudgeMFA.state",
"options": [
{ "label": "Enabled", "value": "enabled" },
{ "label": "Disabled", "value": "disabled" }
]
},
{
"type": "autoComplete",
"multiple": false,
"creatable": false,
"required": false,
"label": "Authentication method to nudge users to register (default is Microsoft Authenticator)",
"name": "standards.NudgeMFA.targetedAuthenticationMethod",
"options": [
{ "label": "Microsoft Authenticator", "value": "microsoftAuthenticator" },
{ "label": "Passkey (FIDO2)", "value": "fido2" }
],
"condition": {
"field": "standards.NudgeMFA.state",
"compareType": "valueEq",
"compareValue": "enabled"
}
},
{
"type": "number",
"name": "standards.NudgeMFA.snoozeDurationInDays",
Expand All @@ -1395,6 +1412,39 @@
"min": { "value": 0, "message": "Minimum value is 0" },
"max": { "value": 14, "message": "Maximum value is 14" }
}
},
{
"type": "switch",
"name": "standards.NudgeMFA.enforceRegistrationAfterAllowedSnoozes",
"label": "Limited number of snoozes (require registration after 3 snoozes)",
"defaultValue": true,
"condition": {
"field": "standards.NudgeMFA.state",
"compareType": "valueEq",
"compareValue": "enabled"
}
},
{
"type": "textField",
"name": "standards.NudgeMFA.includeTargets",
"label": "Include groups (comma separated group names, 'AllUsers' for everyone, blank = keep current targets)",
"required": false,
"condition": {
"field": "standards.NudgeMFA.state",
"compareType": "valueEq",
"compareValue": "enabled"
}
},
{
"type": "textField",
"name": "standards.NudgeMFA.excludeTargets",
"label": "Exclude groups (comma separated group names, blank = keep current exclusions)",
"required": false,
"condition": {
"field": "standards.NudgeMFA.state",
"compareType": "valueEq",
"compareValue": "enabled"
}
}
],
"label": "Sets the state for the request to setup Authenticator",
Expand Down Expand Up @@ -5070,6 +5120,7 @@
},
{
"name": "standards.SPDirectSharing",
"deprecated": true,
"cat": "SharePoint Standards",
"tag": [],
"helpText": "This standard has been deprecated in favor of the Default Sharing Link standard. ",
Expand Down Expand Up @@ -6530,7 +6581,7 @@
"label": "Conditional Access Template",
"cat": "Templates",
"multiple": true,
"disabledFeatures": { "report": true, "warn": true, "remediate": false },
"disabledFeatures": { "report": false, "warn": false, "remediate": false },
"impact": "High Impact",
"addedDate": "2023-12-30",
"tag": [
Expand Down Expand Up @@ -8043,7 +8094,7 @@
"tag": ["CIS M365 7.0.0 (5.2.3.8)", "CIS M365 7.0.0 (5.2.3.9)"],
"appliesToTest": ["CIS_5_2_3_8", "CIS_5_2_3_9"],
"helpText": "**Requires Entra ID P1.** Configures the Entra ID Smart Lockout settings including lockout duration, lockout threshold, and on-premises integration mode.",
"docsDescription": "Configures the Entra ID Smart Lockout policy which protects against brute-force password attacks. Smart Lockout locks out bad actors who try to guess user passwords or use brute-force methods. It recognizes sign-ins from valid users and treats them differently from attackers. Settings include lockout duration (seconds), lockout threshold (failed attempts before lockout), and on-premises password protection mode (Audit or Enforced).",
"docsDescription": "Configures the Entra ID Smart Lockout policy which protects against brute-force password attacks. Smart Lockout locks out bad actors who try to guess user passwords or use brute-force methods. It recognizes sign-ins from valid users and treats them differently from attackers. Settings include lockout duration (seconds), lockout threshold (failed attempts before lockout), and on-premises password protection mode (Audit or Enforce).",
"addedComponent": [
{
"type": "number",
Expand All @@ -8070,7 +8121,7 @@
"label": "On-Premises Mode",
"options": [
{ "label": "Audit", "value": "Audit" },
{ "label": "Enforced", "value": "Enforced" }
{ "label": "Enforce", "value": "Enforce" }
]
}
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ function Push-CIPPDBCacheData {
} else {
Write-Host "Skipping Compliance data collection for $TenantFilter - no required license"
}

if ($DefenderCapable) {
$Tasks.Add(@{
FunctionName = 'ExecCIPPDBCache'
Expand All @@ -219,15 +219,7 @@ function Push-CIPPDBCacheData {
QueueId = $QueueId
QueueName = "DB Cache SharePoint - $TenantFilter"
})
# SharePointSharingLinks runs as its own activity — it's heavy (scans every drive) and
# spawns a child orchestrator (one activity per site) that needs its own time budget.
$Tasks.Add(@{
FunctionName = 'ExecCIPPDBCache'
Name = 'SharePointSharingLinks'
TenantFilter = $TenantFilter
QueueId = $QueueId
QueueName = "DB Cache SharePointSharingLinks - $TenantFilter"
})
# SharePointSharingLinks runs adhoc since it can take a long time to enumerate all sharing links for large tenants
} else {
Write-Host "Skipping SharePoint data collection for $TenantFilter - no required license"
}
Expand Down
23 changes: 17 additions & 6 deletions Modules/CIPPCore/Public/Get-CIPPDrift.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -377,12 +377,23 @@ function Get-CIPPDrift {
$tenantPolicy.policy | Add-Member -MemberType NoteProperty -Name 'URLName' -Value $TenantPolicy.Type -Force
$TenantPolicyName = if ($TenantPolicy.Policy.displayName) { $TenantPolicy.Policy.displayName } else { $TenantPolicy.Policy.name }
foreach ($TemplatePolicy in $TemplateIntuneTemplates) {
$TemplatePolicyName = if ($TemplatePolicy.displayName) { $TemplatePolicy.displayName } else { $TemplatePolicy.name }

if ($TemplatePolicy.displayName -eq $TenantPolicy.Policy.displayName -or
$TemplatePolicy.name -eq $TenantPolicy.Policy.name -or
$TemplatePolicy.displayName -eq $TenantPolicy.Policy.name -or
$TemplatePolicy.name -eq $TenantPolicy.Policy.displayName) {
# Compare displayName-to-displayName and name-to-name (plus the cross pairings,
# since some policy types are captured under one property but deployed under the
# other) but require BOTH sides of each pairing to be non-empty before treating it
# as a match. Most Intune policy types (compliance policies, device configurations,
# group policy configs, etc.) only expose displayName and have no .name property at
# all, so comparing raw .name values directly (as before) compared $null -eq $null,
# which is $true in PowerShell - causing every tenant policy to falsely "match" the
# first template as soon as any template lacked a .name property, suppressing all
# tenant-only deviations. Note: templates always get a .displayName forced onto them
# (see Add-Member above) even for name-only policy types like Settings Catalog
# (deviceManagement/configurationPolicies), so the name-to-name pairing must still be
# compared directly from the raw properties - collapsing to a single "effective name
# preferring displayName" per side would silently break matching for those policies.
if (($TemplatePolicy.displayName -and $TenantPolicy.Policy.displayName -and $TemplatePolicy.displayName -eq $TenantPolicy.Policy.displayName) -or
($TemplatePolicy.name -and $TenantPolicy.Policy.name -and $TemplatePolicy.name -eq $TenantPolicy.Policy.name) -or
($TemplatePolicy.displayName -and $TenantPolicy.Policy.name -and $TemplatePolicy.displayName -eq $TenantPolicy.Policy.name) -or
($TemplatePolicy.name -and $TenantPolicy.Policy.displayName -and $TemplatePolicy.name -eq $TenantPolicy.Policy.displayName)) {
$PolicyFound = $true
break
}
Expand Down
91 changes: 85 additions & 6 deletions Modules/CIPPCore/Public/Get-CIPPTestData.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,80 @@ function Get-CIPPTestData {
Cached wrapper around New-CIPPDbRequest for test functions

.DESCRIPTION
Returns cached tenant data during test suite execution. The cache is
backed by CIPP.TestDataCache (static ConcurrentDictionary in C#) so
it is shared across all PowerShell runspaces within the worker process.
Returns cached tenant data during test suite execution. The cache is backed by
CIPP.TestDataCache (static ConcurrentDictionary in C#) so it is shared across all
PowerShell runspaces within the worker process.

RECORDS ARE FIELD-PROJECTED. Only the fields listed for $Type in
Get-CippTestDataFieldManifest are materialized. Reading an unlisted field returns $null
with no error, so the test emits a wrong verdict silently. If you add a field read to a
test, add it to the manifest first. A type absent from the manifest is fetched whole,
which is the safe default.

Projection is record-level: a kept field keeps its entire subtree, so
`$policy.conditions.users.includeRoles` only requires 'conditions'.

Records are PSCustomObject and must stay so. Hashtable output changes scalar semantics
($x[0] becomes a key lookup returning $null; $x.Count returns the record's field count
instead of 1) and only when a pipeline yields exactly one record — data-dependent, and
easily missed in testing.

.PARAMETER TenantFilter
The tenant domain or GUID to filter by

.PARAMETER Type
The data type to retrieve (e.g., Users, Groups, ConditionalAccessPolicies)

.PARAMETER Fields
Optional. Override the manifest for this call: keep only these top-level fields. Prefer
the manifest; this is for one-off and diagnostic callers, not tests.

The field set is part of the cache key — it must be, or callers asking for the same
tenant+type with different lists would receive each other's projection. Distinct lists
therefore fragment the cache: the same rows parsed once per list, all alive together for
the TTL. Per-call-site lists measured several times worse than one shared per-type entry.
Fields belong to the type — see Get-CippTestDataFieldManifest.

.PARAMETER NoProjection
Return every field, ignoring the manifest.

Required for the custom-script sandbox (Get-CippSandboxData): those scripts are
customer-authored, so the fields they read are unknowable and a manifest built from our
own tests would silently hand them incomplete records. Any caller fetching data on behalf
of code we did not write must pass this.

.EXAMPLE
Get-CIPPTestData -TenantFilter $Tenant -Type 'Users'
Normal use: the manifest decides the fields. This is what tests should do.

.EXAMPLE
Get-CIPPTestData -TenantFilter $Tenant -Type 'Users' -NoProjection
Every field. For callers serving code we did not author.

.NOTES
Verifying a change: "0 errored" proves nothing, because the failure modes here are silent
— a broken test still reports success. Diff test verdicts against a baseline, across more
than one tenant: a bug that needs exactly one matching record will not show on a tenant
that has two.

Useful endpoints: ExecTestRun then ListTests for verdicts; ListDBCache to confirm a field
exists and its real casing before adding it to the manifest (type=_availableTypes lists
the types); ListWorkerHealth?Action=CacheDiag for entries, hit rate and a per-type
breakdown whose keys carry the projected field list.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$TenantFilter,

[Parameter(Mandatory = $false)]
[string]$Type
[string]$Type,

[Parameter(Mandatory = $false)]
[string[]]$Fields,

[Parameter(Mandatory = $false)]
[switch]$NoProjection
)

# Enforce tenant lock when running inside custom script execution
Expand All @@ -32,14 +89,36 @@ function Get-CIPPTestData {
throw 'TenantFilter is required.'
}

$CacheKey = '{0}|{1}' -f $TenantFilter, $Type
# Resolve the field set: an explicit -Fields wins, otherwise consult the per-type manifest.
# -NoProjection suppresses both (the sandbox path must never receive projected records).
$EffectiveFields = if ($NoProjection) {
$null
} elseif ($Fields) {
$Fields
} else {
Get-CippTestDataFieldManifest -Type $Type
}

# Normalize the field set (sorted + de-duplicated) so that callers asking for the same
# fields in a different order share one cache entry instead of parsing twice.
$NormalizedFields = if ($EffectiveFields) { @($EffectiveFields | Sort-Object -Unique) } else { $null }

$CacheKey = if ($NormalizedFields) {
'{0}|{1}|{2}' -f $TenantFilter, $Type, ($NormalizedFields -join ',')
} else {
'{0}|{1}' -f $TenantFilter, $Type
}

$CachedValue = $null
if ([CIPP.TestDataCache]::TryGet($CacheKey, [ref]$CachedValue)) {
return $CachedValue
}

$Data = New-CIPPDbRequest -TenantFilter $TenantFilter -Type $Type
$Data = if ($NormalizedFields) {
New-CIPPDbRequest -TenantFilter $TenantFilter -Type $Type -Fields $NormalizedFields
} else {
New-CIPPDbRequest -TenantFilter $TenantFilter -Type $Type
}

[CIPP.TestDataCache]::Set($CacheKey, $Data)

Expand Down
Loading