feat(halo): link CIPP-generated PSA tickets to the affected user#2
Open
renada-jacob wants to merge 1991 commits into
Open
feat(halo): link CIPP-generated PSA tickets to the affected user#2renada-jacob wants to merge 1991 commits into
renada-jacob wants to merge 1991 commits into
Conversation
fix: remove unneeded stuff
…essage-Branding_OME
Added 10x configurable fields (background colour, logo URL, introduction text, read button text, email text, privacy statement URL, disclaimer text, portal text, OTP enabled, social ID sign-in) and a helpText link to the Microsoft Purview branding documentation. Signed-off-by: Chris Dewey <142454021+chris-dewey-1991@users.noreply.github.com>
…ssigned-admin-roles-to-disable-EXO-standard
Stores the entire MDE mobileThreatDefenseConnector object (heartbeat, per-platform enable/MAM/block flags, MDE attach, iOS metadata flags) in the reporting DB instead of only partnerState. Live HTTP endpoint mirrors the same shape so cached and live responses are interchangeable. Failed Graph calls still write a partnerState=unavailable row so AllTenants reports retain the tenant.
Co-authored-by: Copilot <copilot@github.com>
…Client Co-authored-by: Copilot <copilot@github.com>
Three new standards: Invoke-CIPPStandardDisableEWS (disable Exchange Web Services org-wide), Invoke-CIPPStandardSPDisableCustomScripts (disable custom scripts on SharePoint/OneDrive), and Invoke-CIPPStandardSPDisableStoreAccess (disable SharePoint Store access).
Adds a HaloPSA user lookup helper that resolves a Microsoft 365 end-user to their HaloPSA contact id, scoped to a specific client. Matches by Azure Object ID first (azureoid field on the contact), then falls back to email address. Returns $null when no match is found so callers can decide how to handle unmatched users. Foundation for linking CIPP-generated alert tickets to the affected end-user instead of the client's General User contact. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds optional UserUPN/AzureOID/DisplayName parameters to New-HaloPSATicket. When the HaloPSA.LinkTicketsToUsers setting is enabled and a user identifier is supplied, looks up the matching HaloPSA contact (via Get-HaloUser) and populates userlookup.id and user_name on the ticket payload so the ticket lands directly on the end-user's contact record rather than the client's General User. Unmatched users fall back to the existing userlookup.id = -1 (General User) behaviour, with the affected UPN appended to the ticket description and a warning logged to the CIPP logbook for follow-up. Consolidation hash now includes the UPN when user-linking is active so that per-user tickets for the same alert title don't collapse onto each other. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When an alert payload includes an AffectedUser (UPN, optional AzureOID, optional DisplayName) and HaloPSA.LinkTicketsToUsers is enabled, pass it through to New-HaloPSATicket. Best-effort resolves the user's Azure Object ID via Graph when only the UPN was supplied, so the HaloPSA contact lookup can prefer the more reliable azureoid match. Existing behaviour is unchanged when AffectedUser is absent or the toggle is off. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Lets callers attach an affected-user object (UPN, AzureOID, DisplayName) to PSA alerts. The user is added to the Alert hashtable handed to New-CippExtAlert, which uses it to link the resulting HaloPSA ticket to the matching contact. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When the HaloPSA integration has LinkTicketsToUsers enabled and a scheduled alert returns rows containing a UPN-like field (UserPrincipalName, UPN, userId, Userkey), Send-CIPPScheduledTaskAlert now groups the result rows by that field and emits one PSA call per affected user. Each call carries an AffectedUser payload so the resulting HaloPSA ticket lands on the correct contact. Rows with no UPN value still produce a single tenant-scoped ticket, and any unexpected error in the split path falls back to today's consolidated-ticket behaviour. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PowerShell parses '$ClientId:' as a scope-qualified variable reference,
breaking the script's parser. Wrap the variable in ${} so it's interpolated
correctly inside the warning message.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
HaloPSA rejects tickets with a specific user (userlookup.id != -1) when site_id is null - "Please select a valid Client/Site/User". The General User fallback (id = -1) auto-resolves the site, but a real contact does not. Get-HaloUser now returns both the matched user's id and site_id. New- HaloPSATicket pulls the site_id from that record onto the payload so the ticket can be created against the correct site under the client. Unmatched users still produce site_id = null, preserving today's General User behaviour. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Invoke-RestMethod deserialises JSON numbers as [double] by default, so a
site_id of 95 round-trips as "95.0" - which Halo's ticket endpoint rejects
("Input string '95.0' is not a valid integer"). Cast both id and site_id
to [int] when building the result so ConvertTo-Json emits them as plain
integers.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Brings the feature branch up to date with ~300 dev commits. Both files modified by this feature (Send-CIPPAlert.ps1, Send-CIPPScheduledTaskAlert.ps1) auto-merged with no conflicts. AffectedUser parameter and the per-user PSA splitter are intact alongside the upstream alert pipeline fixes (KelvinTegelaar#2006 et al.). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Audit-log driven alerts (new inbox rule, role change, MFA disabled, sessions revoked, etc.) now extract the affected user from the audit record and pass it through to the PSA pipeline as AffectedUser. When HaloPSA.LinkTicketsToUsers is enabled, the resulting Halo ticket lands on the matching contact instead of the client's General User. Detection prefers ObjectId (target of the action) over UserId/Userkey, taking the resolved UPN from the upstream GUID-mapped CIPP* property when available and using the raw GUID directly as the AzureOID for the Halo lookup. Service principal events naturally fall through with no AffectedUser (no @ in any candidate), preserving the original General User behaviour. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds visibility to the PSA branch of Send-CIPPAlert so callers can see what actually happened: - When sendtoIntegration is disabled in CippNotifications config, log a clear skip reason instead of silently doing nothing. - When an AffectedUser is attached, log which UPN/OID is being targeted. - Capture and surface the result string returned by New-CippExtAlert (which carries through "Ticket created in HaloPSA: <id>" or the failure message). No behavioural change to delivery - just diagnostics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The HaloPSA mapping table stores IntegrationId as a string, which then threads through New-CippExtAlert -> New-HaloPSATicket and lands in the Tickets payload as e.g. "client_id":"57". Halo rejects this with the generic "Please select a valid Client/Site/User" error - the same class of bug we already fixed for site_id. Direct PowerShell test calls used integer literals so they didn't hit this; only the audit-log and scheduled-alert paths (which read the mapping from storage) tripped it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Real-world HaloPSA contacts populated by AD/Azure AD sync often store the user's UPN in 'networklogin' or 'aaduserid' rather than (or in addition to) 'emailaddress'. The previous lookup only checked 'emailaddress', so a contact like Bob whose UPN was synced into 'networklogin' wouldn't match even though the data was right there. Now matches against: AzureOID -> azureoid, aaduserid Email -> emailaddress, networklogin, aaduserid Field lists are defined once at the top so adding more is a one-line change. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
HaloPSA's /Users?search=<term> parameter does not search the azureoid field, so an OID-only search returns zero rows even when a contact has that exact OID populated. Bob's contact in the test sandbox proved this: azureoid was set correctly but emailaddress/networklogin/aaduserid were all blank, so neither the OID search nor the email-field filter found him. Restructured to: 1. Run a search for each supplied term (AzureOID, Email) and merge the results into a deduped candidate pool keyed by user id. 2. Filter the pool preferring AzureOID matches against azureoid/aaduserid, then fall back to email matches against emailaddress/networklogin/ aaduserid. This way the email search brings the contact into scope and the OID filter catches it, even when none of the email-shaped fields are populated on the Halo record. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Halo's basic ?search= parameter only searches a fixed set of indexed fields (name, email, logins...) and notably NOT azureoid. So an OID-only lookup returned zero results even when a contact had that exact OID populated. Switch the AzureOID path to ?advanced_search= with filter_type=2 (equality) against azureoid and aaduserid in turn. This queries the underlying field directly and short-circuits as soon as we find a match, avoiding the previous fetch-and-filter dance for the common case. The email path still uses the basic search but now also checks the azureoid field on returned records, so contacts with azureoid set and blank email fields still match when both terms are supplied. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ilures Two fixes for issues surfaced by real audit-log alerts: 1. Get-HaloUser: some Halo instances don't whitelist azureoid/aaduserid as filterable advanced_search fields, returning "Invalid advanced search parameter(s)". The email-search fallback already handles this case correctly, but every alert was emitting two Warning-level logbook entries. Now only log when the failure is something other than the expected "field not whitelisted" rejection. 2. New-HaloPSATicket: when ConsolidateTickets is on and a note-add to an existing ticket fails (permission error on the configured outcome, ticket type that doesn't accept the action, etc.), the function used to return the error and never create a new ticket. The alert was effectively lost. Now log the failure and fall through to creating a new ticket so the alert reaches the technician one way or another. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…itter Adds a per-scheduled-alert setting that can override the global HaloPSA.LinkTicketsToUsers toggle on a single alert basis. Lets MSPs keep wide alerts like 'users without MFA' as one consolidated ticket per tenant when individual tickets would flood their PSA, while still splitting per-user for the alerts where granularity matters. Stored as a new column 'PsaTicketStrategy' on the ScheduledTasks row with values: - 'split' - always one ticket per affected user - 'consolidated' - always one ticket per tenant - '' / null - inherit the integration's global setting (default) Send-CIPPScheduledTaskAlert resolves the per-task strategy first and only falls back to the global toggle when the task hasn't expressed a preference, so existing alerts created before this change keep working exactly as before.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Today, every CIPP-generated HaloPSA ticket lands on the client's General User contact (
userlookup.id = -1), regardless of which end-user the alert is actually about. This PR adds a single integration toggle that, when enabled, splits per-user alerts into one ticket each and links them to the matching HaloPSA contact - or falls back cleanly to the General User when no match exists.Pairs with the companion CIPP UI changes in https://github.com/renada-jacob/CIPP/pull/feat/halo-link-tickets-to-users.
What's new
HaloPSA.LinkTicketsToUsers(default off, no behavioural change for existing installs until opted in).Get-HaloUserthat looks up a HaloPSA contact by Azure Object ID first (via?advanced_search=againstazureoid/aaduserid), falling back to email/UPN againstemailaddress/networklogin/aaduserid. Cast to[int]so PowerShell doesn't serialise IDs back as95.0.Send-CIPPScheduledTaskAlert- when the toggle is on and a scheduled-alert task returns rows containing a UPN-like field (UserPrincipalName/userPrincipalName/UPN/userId/Userkey), groups by user and emits one PSA call per user with anAffectedUserpayload.AffectedUserparameter onSend-CIPPAlert -Type 'psa'that threads through toNew-CippExtAlertandNew-HaloPSATicket.Invoke-CippWebhookProcessing) now extracts the affected user fromObjectId/UserId/Userkey(and their CIPP-mapped variants) so role-change, password-reset, sessions-revoked, MFA-disabled, inbox-rule etc. tickets land on the right contact too.New-HaloPSATicketpopulatesuserlookup.id,user_nameandsite_idfrom the matched contact when found, and castsclient_idto[int]so storage-resident IDs (which are stored as strings) don't break Halo's payload validation.Send-CIPPAlert: explicit "PSA delivery skipped" message whensendtoIntegrationis off, plus surfaces the result string from the Halo call.Out of scope
Push-SchedulerCIPPNotifications(Send-CIPPAlert -Type 'psa'lines 140-168) is intentionally left unchanged - itsUsernamecolumn is the CIPP operator, not the affected M365 user, so there's no useful identifier to link on.Test plan
Link Tickets to affected Userson the HaloPSA integration page; configure aTicket TypeandOutcome(the latter only required ifConsolidate Ticketsis on).New-HaloPSATicket -Title test -Description '<p>x</p>' -Client <id> -UserUPN <upn> -AzureOID <oid> -DisplayName <name>against a Halo contact that hasazureoidpopulated. Expect ticket linked to that contact at the right site.Send-CIPPScheduledTaskAlert -Results <array of 2 different UPN rows> -TaskInfo <stub with PostExecution='psa'> -TenantFilter <domain> -TaskType Alert. Expect two separate Halo tickets, one per user, each linked to the right contact.Invoke-CippWebhookProcessingwith a fake$DatacontainingObjectId='<upn>',Operation='Disable Strong Authentication.',CIPPAction='{"value":["generatePSA"]}'. Expect ticket linked to that user.Get-CIPPAlertMFAAlertUsers(or any per-user alert) with PSA post-execution. Expect one ticket per user without MFA.🤖 Generated with Claude Code