Skip to content

ConnectWise SkyKick Exposure Check

directorcia edited this page Oct 20, 2026 · 1 revision

Purpose

cw-skykick-exposure-check.ps1 performs a read-only Microsoft Graph review of one or more Microsoft 365 / Microsoft Entra tenants for indicators associated with the ConnectWise Cloud Backup (formerly SkyKick) vendor-credential incident. It looks for known ConnectWise application identities, suspicious service-principal activity, selected directory audit events, recently effective credentials, and Conditional Access exclusions.

The script is an investigation aid, not a forensic determination. A Suspicious verdict means one or more configured indicators matched and requires investigation. A Not present verdict means the checks completed without finding the configured service-principal matches; it does not establish that a tenant was never exposed or that no other indicators exist.

The incident reference currently embedded in the script is the Huntress vendor-credential advisory. The application IDs, owner tenant ID, IP addresses, user-agent patterns, activity names, and date windows are constants or parameters in the script and should be reviewed against current incident intelligence before reuse.

Safety and side effects

All Microsoft Graph requests are issued through Invoke-GraphGet, which calls Invoke-MgGraphRequest with -Method GET. The script does not create, update, disable, or delete tenant objects. Credential values are not retrieved for display; the report contains credential metadata such as type, key ID, display name, and validity dates.

There are local side effects:

  • The script disconnects the current Microsoft Graph session before connecting to each tenant, and disconnects again at the end. Run it in a dedicated PowerShell session if you need to preserve another Graph connection.
  • It creates the report directory if it does not exist and writes three report files there.
  • It can wait between requests when Graph throttles the client.

The script does not open the generated HTML report. Although -Headless appears in the parameter help, it is currently unused and does not change execution.

Requirements

  • PowerShell 7.4 or later.
  • The Microsoft.Graph.Authentication module.
  • Network access to Microsoft Graph.
  • Consent and directory permissions sufficient for the checks being run.
  • Entra ID P1 or P2 in the target tenant to read sign-in logs. That check can be unavailable without the required licensing.

The script requests these delegated scopes for interactive sign-in and documents the corresponding application permissions for certificate authentication:

Permission Used for
Application.Read.All Read app registrations and service principals, including credential metadata.
Directory.Read.All Read directory objects and organization information.
AuditLog.Read.All Read sign-in and directory audit logs.
RoleManagement.Read.Directory Read directory role assignments and role definitions.
Policy.Read.All Read Conditional Access policies.

Install the authentication module for the current user if required:

Install-Module Microsoft.Graph.Authentication -Scope CurrentUser

For unattended certificate authentication, use an application registration with the necessary Microsoft Graph application permissions, tenant admin consent, and a certificate available to the process. For partner access, use an appropriately consented multi-tenant application and provide a tenant-specific row for each customer.

Permission-reporting caveat

The script compares Get-MgContext().Scopes with the required permission names. This is useful for delegated sessions, where scopes are present. App-only certificate contexts may not expose application roles through Scopes; consequently, the report can list permissions as missing even when application permissions were granted. Treat actual Graph request results and per-check errors as authoritative for app-only runs, not this scope comparison alone.

How to run

Run commands from the repository root or provide paths appropriate to the current working directory.

Interactive delegated sign-in

./cw-skykick-exposure-check.ps1 -TenantId contoso.onmicrosoft.com

Use device-code authentication for a headless console or a session where browser sign-in is unavailable:

./cw-skykick-exposure-check.ps1 -TenantId contoso.onmicrosoft.com -UseDeviceCode

Certificate authentication from the local certificate store

Create a CSV with one row per tenant. TenantId, ClientId, and CertificateThumbprint are used for this authentication path; Name is optional.

TenantId,Name,ClientId,CertificateThumbprint
contoso.onmicrosoft.com,Contoso,00000000-0000-0000-0000-000000000000,0123456789ABCDEF0123456789ABCDEF01234567

Run the report:

./cw-skykick-exposure-check.ps1 -TenantCsv ./tenants.csv -OutputPath ./reports

The certificate must be available in a certificate store visible to the running account. The script passes the thumbprint to Connect-MgGraph.

Certificate authentication from a PFX file

Use CertificatePath instead of relying on the local certificate store. Keep the PFX password out of the CSV and inject it through an environment variable from an approved secret manager.

TenantId,Name,ClientId,CertificatePath,CertificateThumbprint,CertificatePasswordEnvironmentVariable
contoso.onmicrosoft.com,Contoso,00000000-0000-0000-0000-000000000000,./certs/contoso.pfx,0123456789ABCDEF0123456789ABCDEF01234567,SECMON_PFX_PASSWORD
$env:SECMON_PFX_PASSWORD = '<injected by your secret manager>'
./cw-skykick-exposure-check.ps1 -TenantCsv ./tenants.csv -CertificatePasswordEnvironmentVariable SECMON_PFX_PASSWORD -OutputPath ./reports

The thumbprint column is optional for PFX authentication; when provided, the script checks that the loaded PFX thumbprint matches it. A row-level CertificatePasswordEnvironmentVariable overrides the script-level default. The password itself is never written to a report or status output.

Tenant objects from PowerShell

-Tenants accepts an array of hashtables or objects using the same properties as the CSV input:

$tenants = @(
    @{
        TenantId = 'contoso.onmicrosoft.com'
        Name = 'Contoso'
        ClientId = '00000000-0000-0000-0000-000000000000'
        CertificateThumbprint = '0123456789ABCDEF0123456789ABCDEF01234567'
    }
)

./cw-skykick-exposure-check.ps1 -Tenants $tenants -OutputPath ./reports

For PFX use, supply CertificatePath and, where needed, CertificatePasswordEnvironmentVariable. The global -CertificatePasswordEnvironmentVariable parameter is the default for rows without an override.

Parameters

Parameter Purpose
-TenantId Tenant GUID or verified domain for a single interactive delegated sign-in.
-UseDeviceCode Use device-code sign-in with -TenantId.
-TenantCsv CSV tenant list for unattended certificate authentication.
-Tenants Array of tenant hashtables or objects for unattended certificate authentication.
-CertificatePasswordEnvironmentVariable Name of the environment variable containing the PFX password; defaults to SECMON_PFX_PASSWORD. This is the variable name, not the password.
-OutputPath Report directory; defaults to the current directory.
-SinceSignIns Inclusive UTC start time for sign-in queries; defaults to 2026-10-01T00:00:00Z.
-SinceAudit Inclusive UTC start time for directory audit and credential-validity checks; defaults to 2026-10-13T00:00:00Z.
-Headless Present in the interface but currently has no effect. The script never opens the report automatically.

Pass alternate windows as PowerShell date/time values, preferably with an explicit UTC offset, for example:

./cw-skykick-exposure-check.ps1 -TenantId contoso.onmicrosoft.com -SinceSignIns '2026-10-05T00:00:00Z' -SinceAudit '2026-10-15T00:00:00Z'

Checks performed

The script processes tenants sequentially. For each tenant it disconnects the existing Graph context, authenticates, records context scopes, attempts to read the organization display name, and then runs the checks below. A connection failure is recorded for that tenant; processing continues with the next tenant.

1. ConnectWise/SkyKick service principals

The script reads service principals and selects matches by any of these conditions:

  • appId matches one of seven hard-coded ConnectWise application IDs.
  • displayName matches ConnectWise or SkyKick.
  • appOwnerOrganizationId matches the hard-coded ConnectWise owner tenant ID.

For each match, it reports the display name, application and object IDs, enabled state, owner tenant, creation timestamp, and whether the application ID is a known indicator. It also attempts to collect:

  • Directory role assignments, including role name and scope.
  • OAuth2 delegated permission grants and their consent type/scopes.
  • App-role assignments, including the resource and role value.

A match holding one of the configured highly privileged directory roles is added to the suspicious findings. Failures in some per-service-principal role or grant lookups are represented in the corresponding report fields; they do not necessarily change the overall ServicePrincipals check status from OK.

2. Service-principal sign-ins

The script queries the Microsoft Graph beta sign-in endpoint for service-principal sign-ins since -SinceSignIns. It searches for each known application ID, each discovered ConnectWise/SkyKick application ID, and each configured indicator IP address. Results are deduplicated by sign-in ID.

It records time, application, application ID, IP address, user agent, resource, status error code, and whether the event matched an indicator. An event is flagged when its IP matches the configured IP list or its user agent matches the configured patterns (Python-urllib, python-requests, or curl/). Sign-in logs require the appropriate license and permissions; this check can be unavailable for that reason.

3. Directory audit and credential validity

The script queries directory audit events from -SinceAudit for these activity names:

  • Add member to role
  • Update application
  • Add service principal credentials
  • Update application – Certificates and secrets management

For update and credential activities, it retains events initiated by an application/service principal. For role additions, it skips user-initiated events and retains app-initiated events. The flagging rule marks a role addition suspicious when the initiator matches ConnectWise or is any application. Review the initiator and target in context; this is a broad heuristic, not proof of compromise.

It also scans all app registrations and service principals for password/certificate credentials whose startDateTime is on or after -SinceAudit. Graph credential objects expose validity dates, not the credential's actual creation time. These rows must therefore be interpreted as credentials whose validity starts in the selected window, not credentials proven to have been created in that window.

4. Conditional Access exclusions

The script reads Conditional Access policies and reports policies whose excluded application IDs or excluded service-principal IDs match a known ConnectWise app ID or a discovered ConnectWise/SkyKick app/object ID. This is an inventory of exclusions; it does not determine whether a policy applies to a particular sign-in or whether the policy otherwise protects the tenant.

Verdicts and recommendations

Verdict selection follows this order:

Verdict Meaning in this script
Suspicious At least one configured suspicious condition was recorded: a privileged role, a flagged sign-in, or a flagged audit event.
Present-no-activity One or more matching service principals were found, but no suspicious condition was recorded. This does not mean the integrations are safe.
Not present The service-principal check completed successfully and found no matching service principals. Other checks may still be unavailable or may have found credential or policy observations.
Unknown The service-principal check did not complete successfully and no stronger verdict was produced.
Error Tenant authentication/connection failed before checks could run.

Credential-validity observations and Conditional Access exclusions generate recommendations, but do not independently change the verdict to Suspicious. The verdict is a triage summary; review all check statuses, findings, and recommendations. The script may recommend disabling a matching enterprise app, but it does not perform that action.

Reports

The script writes files named with a local-time timestamp under -OutputPath:

cw-skykick-exposure_yyyyMMdd_HHmmss.csv
cw-skykick-exposure_yyyyMMdd_HHmmss.json
cw-skykick-exposure_yyyyMMdd_HHmmss.html
  • CSV: one summary row per tenant, including verdict, finding counts, unavailable checks, missing-permission text, and connection error.
  • JSON: detailed tenant results, including check statuses, permissions, all findings, and recommendations.
  • HTML: a readable version of the detailed report. Dynamic cell and recommendation content is HTML-encoded.

Times in detailed findings are converted to the Australia/Sydney time zone when available, falling back to the machine's local time zone. The current formatter appends the literal AET; during daylight-saving periods Sydney observes AEDT, so that suffix may not accurately describe the offset. The filename timestamp uses the machine's local time.

Reliability behavior and limits

  • Graph collection follows @odata.nextLink and retries HTTP 429, 503, and 504 errors with exponential waits, honoring Retry-After when available. It allows up to 200 pages per request and fails the check if another page remains, rather than treating truncated results as complete.
  • Check-level errors are recorded and generally do not stop later checks or other tenants. A tenant connection error is isolated to that tenant.
  • The sign-in endpoint is the Graph beta API. Microsoft can change beta behavior or schemas.
  • Detection is limited to the hard-coded IDs, owner tenant, names, IP addresses, user-agent patterns, audit activity names, selected date windows, and readable logs. It does not replace broader incident response or review of other indicators.
  • The script does not open the HTML report; open it manually from the output directory after the run.

Operational review checklist

  1. Confirm the indicator values and date windows are current for the incident being investigated.
  2. Confirm the authentication application and certificate are approved, available, and consented in every target tenant.
  3. Run first against a known test tenant or a small tenant list and inspect all three report formats.
  4. Review unavailable checks and permission output before interpreting a clean verdict.
  5. Investigate every suspicious match with the relevant tenant's audit and sign-in context; do not treat the script's recommended actions as automatically approved changes.

Clone this wiki locally