Data/Rules.ps1
|
# Audit rules. Each rule declares the coverage keys it needs (Requires); if any of them wasn't collected, the # rule is reported as not evaluated rather than run on incomplete data. Severity is the rule's maximum; some # rules emit individual findings at a lower severity. Evaluate receives ($Ctx, $Rule) and writes findings # created with New-AgentIdFinding. A rule may also define Applies, a scriptblock taking $Ctx: when it returns # false the rule is skipped silently (used by rules that need configuration from the caller). $ref = @{ BlueprintCredentials = 'https://learn.microsoft.com/en-us/entra/agent-id/identity-platform/create-blueprint?tabs=microsoft-graph-api#configure-credentials-for-the-agent-identity-blueprint' BlockedPermissions = 'https://learn.microsoft.com/en-us/graph/api/resources/agentid-platform-overview?view=graph-rest-beta#microsoft-graph-permissions-blocked-for-agents' Sponsors = 'https://learn.microsoft.com/en-us/entra/agent-id/identity-platform/agent-owners-sponsors-managers' Governance = 'https://learn.microsoft.com/en-us/entra/id-governance/agent-id-governance-overview' Inheritable = 'https://learn.microsoft.com/en-us/graph/api/resources/inheritablepermission?view=graph-rest-beta' Risk = 'https://learn.microsoft.com/en-us/graph/api/resources/riskyagent?view=graph-rest-beta' PrivilegedRoles = 'https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/privileged-roles-permissions' Consent = 'https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/manage-application-permissions' Blueprint = 'https://learn.microsoft.com/en-us/graph/api/resources/agentidentityblueprint?view=graph-rest-beta' AgentUser = 'https://learn.microsoft.com/en-us/graph/api/resources/agentuser?view=graph-rest-beta' SecurityAttributes = 'https://learn.microsoft.com/en-us/entra/fundamentals/custom-security-attributes-overview' } @( # ------------------------------------------------------------------ Credentials @{ Id = 'AID-CRED-001' Severity = 'High' Category = 'Credentials' Title = 'Blueprint authenticates with a client secret' Requires = @('Blueprints') Reference = $ref.BlueprintCredentials Remediation = 'Replace the secret with a federated identity credential (for example, a managed identity) or a certificate kept in a key vault, then remove the secret.' Evaluate = { param($Ctx, $Rule) foreach ($bp in @($Ctx.Snapshot.Blueprints)) { $active = @(Get-AgentIdActiveCredential $Ctx $bp.passwordCredentials) if (-not $active) { continue } $who = Get-AgentIdBlueprintAgentText $Ctx $bp.appId New-AgentIdFinding -Rule $Rule -ObjectType 'Blueprint' -ObjectId $bp.id -ObjectName $bp.displayName ` -Detail ("{0} active client secret(s). A blueprint credential is a master key: anyone holding a copy of the secret can request tokens for {1}." -f $active.Count, $who) } } } @{ Id = 'AID-CRED-002' Severity = 'Medium' Category = 'Credentials' Title = 'Long-lived blueprint credential' Requires = @('Blueprints') Reference = $ref.BlueprintCredentials Remediation = 'Issue credentials with a lifetime that matches your rotation policy, or move to a federated identity credential, which has no expiry to manage.' Evaluate = { param($Ctx, $Rule) $max = $Ctx.Options.MaxCredentialLifetimeDays foreach ($bp in @($Ctx.Snapshot.Blueprints)) { $long = foreach ($c in @(Get-AgentIdActiveCredential $Ctx (@($bp.passwordCredentials) + @($bp.keyCredentials)))) { $start = ConvertTo-AgentIdDate $c.startDateTime $end = ConvertTo-AgentIdDate $c.endDateTime if ($start -and $end) { $days = [int][math]::Floor(($end - $start).TotalDays) if ($days -gt $max) { '{0}, valid for {1:N0} days until {2}' -f (Format-AgentIdCredential $c), $days, (Format-AgentIdDate $c.endDateTime) } } } if (-not $long) { continue } New-AgentIdFinding -Rule $Rule -ObjectType 'Blueprint' -ObjectId $bp.id -ObjectName $bp.displayName ` -Detail ("Credential lifetime exceeds {0} days: {1}." -f $max, ($long -join '; ')) } } } @{ Id = 'AID-CRED-003' Severity = 'Low' Category = 'Credentials' Title = 'Expired credentials still attached to blueprint' Requires = @('Blueprints') Reference = $ref.BlueprintCredentials Remediation = 'Remove expired credentials so the credential list shows only what is actually in use.' Evaluate = { param($Ctx, $Rule) foreach ($bp in @($Ctx.Snapshot.Blueprints)) { $expired = @(Get-AgentIdExpiredCredential $Ctx (@($bp.passwordCredentials) + @($bp.keyCredentials))) if (-not $expired) { continue } $list = ($expired | ForEach-Object { '{0}, expired {1}' -f (Format-AgentIdCredential $_), (Format-AgentIdDate $_.endDateTime) }) -join '; ' New-AgentIdFinding -Rule $Rule -ObjectType 'Blueprint' -ObjectId $bp.id -ObjectName $bp.displayName ` -Detail ("{0} expired credential(s): {1}." -f $expired.Count, $list) } } } @{ Id = 'AID-CRED-004' Severity = 'Low' Category = 'Credentials' Title = 'Blueprint has multiple active credentials' Requires = @('Blueprints') Reference = $ref.BlueprintCredentials Remediation = 'Keep one active credential, plus a second only during a planned rotation.' Evaluate = { param($Ctx, $Rule) foreach ($bp in @($Ctx.Snapshot.Blueprints)) { $secrets = @(Get-AgentIdActiveCredential $Ctx $bp.passwordCredentials) $certs = @(Get-AgentIdActiveCredential $Ctx $bp.keyCredentials) $total = $secrets.Count + $certs.Count if ($total -le 1) { continue } New-AgentIdFinding -Rule $Rule -ObjectType 'Blueprint' -ObjectId $bp.id -ObjectName $bp.displayName ` -Detail ("{0} active credentials ({1} secret(s), {2} certificate(s)). Each one is an independent way to act as {3}." -f $total, $secrets.Count, $certs.Count, (Get-AgentIdBlueprintAgentText $Ctx $bp.appId)) } } } @{ Id = 'AID-CRED-005' Severity = 'Medium' Category = 'Credentials' Title = 'Agent identity has its own credentials' Requires = @('AgentIdentities') Reference = $ref.BlueprintCredentials Remediation = 'Confirm why the agent identity carries its own credential. Agents normally obtain tokens through their blueprint; remove credentials that are not required.' Evaluate = { param($Ctx, $Rule) foreach ($agent in @($Ctx.Snapshot.AgentIdentities)) { $active = @(Get-AgentIdActiveCredential $Ctx (@($agent.passwordCredentials) + @($agent.keyCredentials))) if (-not $active) { continue } New-AgentIdFinding -Rule $Rule -ObjectType 'Agent identity' -ObjectId $agent.id -ObjectName $agent.displayName ` -Detail ("{0} active credential(s) on the agent identity itself: {1}. This is a way to act as the agent that bypasses its blueprint." -f $active.Count, (($active | ForEach-Object { Format-AgentIdCredential $_ }) -join ', ')) } } } @{ Id = 'AID-CRED-006' Severity = 'Info' Category = 'Credentials' Title = 'Blueprint credentials expire soon' Requires = @('Blueprints', 'BlueprintFederatedCredentials') Reference = $ref.BlueprintCredentials Remediation = 'Add a replacement credential before the current ones expire, or switch to a federated identity credential.' Evaluate = { param($Ctx, $Rule) $horizon = $Ctx.AsOf.AddDays($Ctx.Options.ExpiringWithinDays) foreach ($bp in @($Ctx.Snapshot.Blueprints)) { if ($null -eq $bp.federatedIdentityCredentials -or @($bp.federatedIdentityCredentials).Count -gt 0) { continue } $active = @(Get-AgentIdActiveCredential $Ctx (@($bp.passwordCredentials) + @($bp.keyCredentials))) if (-not $active) { continue } $ends = @($active | ForEach-Object { ConvertTo-AgentIdDate $_.endDateTime }) if ($ends -contains $null) { continue } $last = ($ends | Measure-Object -Maximum).Maximum if ($last -gt $horizon) { continue } New-AgentIdFinding -Rule $Rule -ObjectType 'Blueprint' -ObjectId $bp.id -ObjectName $bp.displayName ` -Detail ("Every credential expires by {0}. After that, {1} can no longer get tokens." -f $last.UtcDateTime.ToString('yyyy-MM-dd'), (Get-AgentIdBlueprintAgentText $Ctx $bp.appId)) } } } # ------------------------------------------------------------------ Permissions @{ Id = 'AID-PERM-001' Severity = 'Critical' Category = 'Permissions' Title = 'Agent holds a permission Microsoft blocks for agents' Requires = @('AgentIdentities', 'AgentGrants', 'BlueprintPrincipals', 'BlueprintPrincipalGrants') Reference = $ref.BlockedPermissions Remediation = 'Remove the grant, then establish how it was made: Microsoft blocks these permissions for agents, so the grant predates the block or was made through a path the block does not cover.' Evaluate = { param($Ctx, $Rule) $rows = @(Get-AgentIdEvaluatedAccessRow $Ctx | Where-Object { Test-AgentIdBlockedPermission $Ctx $_ }) foreach ($group in ($rows | Group-Object PrincipalId)) { $first = $group.Group[0] $detail = 'Holds {0}.' -f (Format-AgentIdPermissionList $Ctx $group.Group) if ($first.PrincipalKind -eq 'BlueprintPrincipal') { $detail += ' Access granted to a blueprint principal can be inherited by ' + (Get-AgentIdBlueprintAgentText $Ctx $first.BlueprintAppId) + '.' } New-AgentIdFinding -Rule $Rule -ObjectType (Get-AgentIdPrincipalLabel $first.PrincipalKind) -ObjectId $first.PrincipalId -ObjectName $first.PrincipalName -Detail $detail } } } @{ Id = 'AID-PERM-002' Severity = 'High' Category = 'Permissions' Title = 'Agent holds a sensitive application permission' Requires = @('AgentIdentities', 'AgentGrants', 'BlueprintPrincipals', 'BlueprintPrincipalGrants') Reference = $ref.Consent Remediation = 'Replace tenant-wide application permissions with delegated access (on behalf of a user), or scope the access down (for example, an Exchange application access policy or SharePoint Sites.Selected).' Evaluate = { param($Ctx, $Rule) $rows = @(Get-AgentIdEvaluatedAccessRow $Ctx | Where-Object { $_.PermissionType -eq 'Application' -and -not (Test-AgentIdBlockedPermission $Ctx $_) -and (Test-AgentIdSensitivePermission $Ctx 'Application' $_.ResourceAppId $_.Permission) }) foreach ($group in ($rows | Group-Object PrincipalId)) { $first = $group.Group[0] $detail = 'Application permissions {0}. These apply across the whole tenant, with no user present.' -f (Format-AgentIdPermissionList $Ctx $group.Group) if ($first.PrincipalKind -eq 'BlueprintPrincipal') { $detail += ' Access granted to a blueprint principal can be inherited by ' + (Get-AgentIdBlueprintAgentText $Ctx $first.BlueprintAppId) + '.' } New-AgentIdFinding -Rule $Rule -ObjectType (Get-AgentIdPrincipalLabel $first.PrincipalKind) -ObjectId $first.PrincipalId -ObjectName $first.PrincipalName -Detail $detail } } } @{ Id = 'AID-PERM-003' Severity = 'Medium' Category = 'Permissions' Title = 'Sensitive delegated permission consented for all users' Requires = @('AgentIdentities', 'AgentGrants', 'BlueprintPrincipals', 'BlueprintPrincipalGrants') Reference = $ref.Consent Remediation = 'Confirm the agent needs these permissions for every user. Where it does not, replace the tenant-wide (AllPrincipals) grant with per-user consent or narrower scopes.' Evaluate = { param($Ctx, $Rule) $rows = @(Get-AgentIdEvaluatedAccessRow $Ctx | Where-Object { $_.PermissionType -eq 'Delegated' -and $_.ConsentType -eq 'AllPrincipals' -and -not (Test-AgentIdBlockedPermission $Ctx $_) -and (Test-AgentIdSensitivePermission $Ctx 'Delegated' $_.ResourceAppId $_.Permission) }) foreach ($group in ($rows | Group-Object PrincipalId)) { $first = $group.Group[0] $detail = 'Delegated permissions {0}, consented for all users. The agent can use them on behalf of any user, without that user being asked.' -f (Format-AgentIdPermissionList $Ctx $group.Group) if ($first.PrincipalKind -eq 'BlueprintPrincipal') { $detail += ' Access granted to a blueprint principal can be inherited by ' + (Get-AgentIdBlueprintAgentText $Ctx $first.BlueprintAppId) + '.' } New-AgentIdFinding -Rule $Rule -ObjectType (Get-AgentIdPrincipalLabel $first.PrincipalKind) -ObjectId $first.PrincipalId -ObjectName $first.PrincipalName -Detail $detail } } } @{ Id = 'AID-PERM-004' Severity = 'High' Category = 'Permissions' Title = 'Blueprint lets agents inherit broad delegated permissions' Requires = @('Blueprints', 'BlueprintInheritablePermissions') Reference = $ref.Inheritable Remediation = 'List the specific scopes agents need (enumerated) instead of allowing all of them, and keep sensitive scopes off the inheritable list.' Evaluate = { param($Ctx, $Rule) foreach ($bp in @($Ctx.Snapshot.Blueprints)) { if ($null -eq $bp.inheritablePermissions) { continue } $who = Get-AgentIdBlueprintAgentText $Ctx $bp.appId foreach ($p in @($bp.inheritablePermissions)) { if (-not $p) { continue } $resource = Get-AgentIdResourceName $Ctx $p.resourceAppId if ($p.kind -eq 'allAllowed') { New-AgentIdFinding -Rule $Rule -ObjectType 'Blueprint' -ObjectId $bp.id -ObjectName $bp.displayName -Severity 'High' ` -Detail ("Inherits all delegated permissions of {0} that the blueprint is granted, without further consent, for {1}." -f $resource, $who) continue } $sensitive = @($p.scopes | Where-Object { Test-AgentIdSensitivePermission $Ctx 'Delegated' $p.resourceAppId $_ }) if ($sensitive) { New-AgentIdFinding -Rule $Rule -ObjectType 'Blueprint' -ObjectId $bp.id -ObjectName $bp.displayName -Severity 'Medium' ` -Detail ("Sensitive inheritable scopes of {0}: {1}, inherited without further consent by {2}." -f $resource, ($sensitive -join ', '), $who) } } } } } @{ Id = 'AID-PERM-005' Severity = 'Critical' Category = 'Permissions' Title = 'Agent holds a directory role' Requires = @('AgentIdentities', 'BlueprintPrincipals', 'AgentUsers', 'DirectoryRoles') Reference = $ref.PrivilegedRoles Remediation = 'Remove directory roles from agents. Where an agent must administer something, use the narrowest role available, scope it to an administrative unit, and make the assignment time-bound.' Evaluate = { param($Ctx, $Rule) $principals = @{} foreach ($a in @($Ctx.Snapshot.AgentIdentities)) { if ($a) { $principals[$a.id] = @('Agent identity', $a.displayName) } } foreach ($u in @($Ctx.Snapshot.AgentUsers)) { if ($u) { $principals[$u.id] = @('Agent user', $u.displayName) } } foreach ($p in @($Ctx.Snapshot.BlueprintPrincipals)) { if ($p) { $principals[$p.id] = @('Blueprint principal', $p.displayName) } } $fallback = $Ctx.Sensitive.PrivilegedRoleTemplateIds $assignments = @($Ctx.Snapshot.DirectoryRoleAssignments | Where-Object { $_ -and $principals.ContainsKey("$($_.principalId)") }) foreach ($group in ($assignments | Group-Object principalId)) { $info = $principals[$group.Name] $privileged = $false $roles = foreach ($ra in $group.Group) { $isPriv = ($ra.isPrivileged -eq $true) -or ($ra.roleTemplateId -and $fallback.ContainsKey("$($ra.roleTemplateId)")) if ($isPriv) { $privileged = $true } $scope = if (-not $ra.directoryScopeId -or $ra.directoryScopeId -eq '/') { 'tenant-wide' } else { "scope $($ra.directoryScopeId)" } $label = if ($isPriv) { 'privileged, ' } else { '' } "$($ra.roleName) ($label$scope)" } $sev = if ($privileged) { 'Critical' } else { 'Medium' } New-AgentIdFinding -Rule $Rule -ObjectType $info[0] -ObjectId $group.Name -ObjectName $info[1] -Severity $sev ` -Detail ('Directory roles: {0}.' -f ($roles -join ', ')) } } } # ------------------------------------------------------------------ Governance @{ Id = 'AID-GOV-001' Severity = 'Medium' Category = 'Governance' Title = 'Agent identity has no sponsor' Requires = @('AgentIdentities', 'AgentSponsors') Reference = $ref.Sponsors Remediation = 'Assign a sponsor: a person accountable for the agent''s access and lifecycle. If no one will take that on, disable the agent.' Evaluate = { param($Ctx, $Rule) foreach ($agent in @($Ctx.Snapshot.AgentIdentities)) { if ($null -eq $agent.sponsors -or @($agent.sponsors).Count -gt 0) { continue } New-AgentIdFinding -Rule $Rule -ObjectType 'Agent identity' -ObjectId $agent.id -ObjectName $agent.displayName ` -Detail 'No sponsor is assigned, so no person is accountable for this agent. A sponsor is required when an agent identity is created, so the original sponsor was most likely removed or deleted.' } } } @{ Id = 'AID-GOV-002' Severity = 'Medium' Category = 'Governance' Title = 'Sponsor account is disabled' Requires = @('AgentSponsors') Reference = $ref.Sponsors Remediation = 'Transfer sponsorship to an active person, for example the former sponsor''s manager. Lifecycle workflows can automate this when people leave.' Evaluate = { param($Ctx, $Rule) $sets = @( @('Agent identity', $Ctx.Snapshot.AgentIdentities), @('Blueprint', $Ctx.Snapshot.Blueprints), @('Agent user', $Ctx.Snapshot.AgentUsers) ) foreach ($set in $sets) { foreach ($obj in @($set[1])) { if (-not $obj -or $null -eq $obj.sponsors) { continue } $disabled = @($obj.sponsors | Where-Object { $_ -and $_.accountEnabled -eq $false }) if (-not $disabled) { continue } $names = ($disabled | ForEach-Object { if ($_.userPrincipalName) { "$($_.displayName) <$($_.userPrincipalName)>" } else { $_.displayName } }) -join ', ' $others = @($obj.sponsors).Count - $disabled.Count $tail = if ($others -eq 0) { ' No active sponsor remains.' } else { " $others other sponsor(s) remain." } New-AgentIdFinding -Rule $Rule -ObjectType $set[0] -ObjectId $obj.id -ObjectName $obj.displayName ` -Detail ("Disabled sponsor(s): {0}.{1}" -f $names, $tail) } } } } @{ Id = 'AID-GOV-003' Severity = 'Medium' Category = 'Governance' Title = 'Blueprint has no sponsor' Requires = @('Blueprints', 'BlueprintSponsors') Reference = $ref.Sponsors Remediation = 'Assign a sponsor to the blueprint: the person accountable for every agent created from it.' Evaluate = { param($Ctx, $Rule) foreach ($bp in @($Ctx.Snapshot.Blueprints)) { if ($null -eq $bp.sponsors -or @($bp.sponsors).Count -gt 0) { continue } New-AgentIdFinding -Rule $Rule -ObjectType 'Blueprint' -ObjectId $bp.id -ObjectName $bp.displayName ` -Detail ('No sponsor is assigned. No person is accountable for {0}.' -f (Get-AgentIdBlueprintAgentText $Ctx $bp.appId)) } } } @{ Id = 'AID-GOV-004' Severity = 'Medium' Category = 'Governance' Title = 'Agent identity''s blueprint is not in this tenant' Requires = @('AgentIdentities', 'BlueprintPrincipals') Reference = $ref.Blueprint Remediation = 'Establish whether the agent is still in use. If its blueprint was removed deliberately, delete the agent identity as well.' Evaluate = { param($Ctx, $Rule) foreach ($agent in @($Ctx.Snapshot.AgentIdentities)) { $bpAppId = "$($agent.agentIdentityBlueprintId)" if (-not $bpAppId -or $Ctx.BlueprintPrincipalByAppId.ContainsKey($bpAppId)) { continue } New-AgentIdFinding -Rule $Rule -ObjectType 'Agent identity' -ObjectId $agent.id -ObjectName $agent.displayName ` -Detail ("References blueprint appId {0}, but no blueprint principal with that appId exists in this tenant. The agent is likely orphaned." -f $bpAppId) } } } @{ Id = 'AID-GOV-005' Severity = 'Low' Category = 'Governance' Title = 'Other applications can create agents from this blueprint' Requires = @('Blueprints') Reference = $ref.Blueprint Remediation = 'Confirm each manager application is expected. A manager application can create agent identities and agent users from the blueprint, so treat it as privileged.' Evaluate = { param($Ctx, $Rule) foreach ($bp in @($Ctx.Snapshot.Blueprints)) { $managers = @($bp.managerApplications | Where-Object { $_ }) if (-not $managers) { continue } New-AgentIdFinding -Rule $Rule -ObjectType 'Blueprint' -ObjectId $bp.id -ObjectName $bp.displayName ` -Detail ('{0} manager application(s) can create agent identities from this blueprint: {1}.' -f $managers.Count, ($managers -join ', ')) } } } @{ Id = 'AID-GOV-006' Severity = 'Info' Category = 'Governance' Title = 'Blueprint is available to other tenants' Requires = @('Blueprints') Reference = $ref.Blueprint Remediation = 'If agents from this blueprint are only meant for this organization, set signInAudience to AzureADMyOrg.' Evaluate = { param($Ctx, $Rule) foreach ($bp in @($Ctx.Snapshot.Blueprints)) { if (-not $bp.signInAudience -or $bp.signInAudience -eq 'AzureADMyOrg') { continue } New-AgentIdFinding -Rule $Rule -ObjectType 'Blueprint' -ObjectId $bp.id -ObjectName $bp.displayName ` -Detail ("signInAudience is {0}, so the blueprint can be added to other tenants." -f $bp.signInAudience) } } } @{ Id = 'AID-GOV-007' Severity = 'High' Category = 'Governance' Title = 'Microsoft has disabled this object' Requires = @('Blueprints') Reference = $ref.Blueprint Remediation = 'Investigate the agent''s behavior before taking any other action. Microsoft sets this status for violations of its service agreement, which can include suspicious or malicious activity.' Evaluate = { param($Ctx, $Rule) $sets = @(@('Blueprint', $Ctx.Snapshot.Blueprints), @('Agent identity', $Ctx.Snapshot.AgentIdentities)) foreach ($set in $sets) { foreach ($obj in @($set[1])) { if (-not $obj -or -not $obj.disabledByMicrosoftStatus -or $obj.disabledByMicrosoftStatus -eq 'NotDisabled') { continue } New-AgentIdFinding -Rule $Rule -ObjectType $set[0] -ObjectId $obj.id -ObjectName $obj.displayName ` -Detail ("disabledByMicrosoftStatus is {0}." -f $obj.disabledByMicrosoftStatus) } } } } @{ # Applies only when the caller names attributes (-RequiredSecurityAttribute) and the snapshot was # collected with -IncludeSecurityAttributes. Id = 'AID-GOV-008' Severity = 'Medium' Category = 'Governance' Title = 'Agent identity is missing required custom security attributes' Requires = @('AgentIdentities', 'AgentSecurityAttributes') Reference = $ref.SecurityAttributes Remediation = 'Assign a value for each listed attribute to the agent identity (this needs the Attribute Assignment Administrator role), or take the attribute off the required list if it does not apply to agents.' Applies = { param($Ctx) @($Ctx.Options.RequiredSecurityAttribute).Count -gt 0 } Evaluate = { param($Ctx, $Rule) foreach ($row in @(Get-AgentIdAgentMissingSecurityAttribute $Ctx $Ctx.Options.RequiredSecurityAttribute)) { New-AgentIdFinding -Rule $Rule -ObjectType 'Agent identity' -ObjectId $row.Agent.id -ObjectName $row.Agent.displayName ` -Detail ('No value for required custom security attribute(s): {0}.' -f ($row.Missing -join ', ')) } } } @{ Id = 'AID-GOV-009' Severity = 'Info' Category = 'Governance' Title = 'Agent identity is missing optional custom security attributes' Requires = @('AgentIdentities', 'AgentSecurityAttributes') Reference = $ref.SecurityAttributes Remediation = 'Assign a value for each listed attribute if it applies to this agent. Optional attributes are advisory; take one off the list if it is rarely used.' Applies = { param($Ctx) @($Ctx.Options.OptionalSecurityAttribute).Count -gt 0 } Evaluate = { param($Ctx, $Rule) foreach ($row in @(Get-AgentIdAgentMissingSecurityAttribute $Ctx $Ctx.Options.OptionalSecurityAttribute)) { New-AgentIdFinding -Rule $Rule -ObjectType 'Agent identity' -ObjectId $row.Agent.id -ObjectName $row.Agent.displayName ` -Detail ('No value for optional custom security attribute(s): {0}.' -f ($row.Missing -join ', ')) } } } # ------------------------------------------------------------------ Agent users @{ Id = 'AID-USER-001' Severity = 'Medium' Category = 'Agent users' Title = 'Agent user has no sponsor' Requires = @('AgentUsers', 'AgentUserSponsors') Reference = $ref.Sponsors Remediation = 'Assign a sponsor to the agent user, or remove the agent user if it is no longer needed.' Evaluate = { param($Ctx, $Rule) foreach ($u in @($Ctx.Snapshot.AgentUsers)) { if ($null -eq $u.sponsors -or @($u.sponsors).Count -gt 0) { continue } New-AgentIdFinding -Rule $Rule -ObjectType 'Agent user' -ObjectId $u.id -ObjectName $u.displayName ` -Detail ("No sponsor is assigned to agent user {0}." -f $u.userPrincipalName) } } } @{ Id = 'AID-USER-002' Severity = 'Medium' Category = 'Agent users' Title = 'Agent user''s parent agent identity not found' Requires = @('AgentUsers', 'AgentIdentities') Reference = $ref.AgentUser Remediation = 'Establish whether the agent user is still in use. An agent user whose agent identity is gone should be disabled and removed.' Evaluate = { param($Ctx, $Rule) foreach ($u in @($Ctx.Snapshot.AgentUsers)) { $parent = "$($u.identityParentId)" if (-not $parent -or $Ctx.AgentById.ContainsKey($parent)) { continue } New-AgentIdFinding -Rule $Rule -ObjectType 'Agent user' -ObjectId $u.id -ObjectName $u.displayName ` -Detail ("identityParentId {0} does not match any agent identity in this tenant; the agent user is likely orphaned. Enabled: {1}." -f $parent, $u.accountEnabled) } } } # ------------------------------------------------------------------ Lifecycle @{ Id = 'AID-LIFE-001' Severity = 'Medium' Category = 'Lifecycle' Title = 'Agent identity has not signed in recently' Requires = @('AgentIdentities', 'SignInActivity') Reference = $ref.Governance Remediation = 'Confirm with the sponsor whether the agent is still needed. Disable or delete unused agents; their permissions remain usable for as long as they exist.' Evaluate = { param($Ctx, $Rule) $days = $Ctx.Options.StaleAfterDays $threshold = $Ctx.AsOf.AddDays(-$days) foreach ($agent in @($Ctx.Snapshot.AgentIdentities)) { if (-not $agent.appId -or -not $Ctx.SignInByAppId.ContainsKey("$($agent.appId)")) { continue } $created = ConvertTo-AgentIdDate $agent.createdDateTime if ($created -and $created -gt $threshold) { continue } $last = ConvertTo-AgentIdDate $Ctx.SignInByAppId[$agent.appId].lastSignInDateTime if ($last -and $last -gt $threshold) { continue } $lastText = if ($last) { "last sign-in $($last.UtcDateTime.ToString('yyyy-MM-dd'))" } else { 'no sign-in recorded' } New-AgentIdFinding -Rule $Rule -ObjectType 'Agent identity' -ObjectId $agent.id -ObjectName $agent.displayName ` -Detail ("No sign-in in the last {0} days ({1}). Enabled: {2}." -f $days, $lastText, $agent.accountEnabled) } } } @{ Id = 'AID-LIFE-002' Severity = 'Low' Category = 'Lifecycle' Title = 'Disabled agent identity still holds permissions' Requires = @('AgentIdentities', 'AgentGrants') Reference = $ref.Governance Remediation = 'Remove permissions from agents being retired. Re-enabling a disabled agent restores all of its access at once.' Evaluate = { param($Ctx, $Rule) foreach ($agent in @($Ctx.Snapshot.AgentIdentities)) { if ($agent.accountEnabled -ne $false) { continue } $direct = @($Ctx.AccessRows | Where-Object { $_.PrincipalId -eq $agent.id -and $_.Source -eq 'Direct' }) if (-not $direct) { continue } New-AgentIdFinding -Rule $Rule -ObjectType 'Agent identity' -ObjectId $agent.id -ObjectName $agent.displayName ` -Detail ("Disabled, but still holds {0} direct permission(s): {1}." -f $direct.Count, (Format-AgentIdPermissionList $Ctx $direct)) } } } @{ Id = 'AID-LIFE-003' Severity = 'Critical' Category = 'Lifecycle' Title = 'Identity Protection flags this agent as risky' Requires = @('RiskyAgents') Reference = $ref.Risk Remediation = 'Investigate the risk detections for this agent. Disable it while investigating if the risk is confirmed, then confirm or dismiss the risk so policies respond correctly.' Evaluate = { param($Ctx, $Rule) foreach ($r in @($Ctx.Snapshot.RiskyAgents)) { if (-not $r -or $r.riskState -notin 'atRisk', 'confirmedCompromised') { continue } $sev = if ($r.riskState -eq 'confirmedCompromised') { 'Critical' } elseif ($r.riskLevel -eq 'high') { 'High' } else { 'Medium' } $type = switch ($r.identityType) { 'agentUser' { 'Agent user' } 'agentIdentityBlueprintPrincipal' { 'Blueprint principal' } default { 'Agent identity' } } New-AgentIdFinding -Rule $Rule -ObjectType $type -ObjectId $r.id -ObjectName $r.agentDisplayName -Severity $sev ` -Detail ("Risk state {0}, risk level {1}, last updated {2}. Detail: {3}." -f $r.riskState, $r.riskLevel, (Format-AgentIdDate $r.riskLastModifiedDateTime), $r.riskDetail) } } } ) |