PingOne (Ping Identity) Identity Security Posture Management (ISPM)

Console-only posture scanning for a PingOne environment: 25 weighted Management API checks in nine categories, run from the dashboard Posture tab and scored 0-100.

Last reviewed September 16, 2026Fresh

Overview

PasskeyBridge reads a PingOne environment through the PingOne Management API, runs 25 checks against its sign-on policies, users, applications, password policies, certificates and risk policies, and stores each one as a finding with a weight, a status and evidence. The dashboard renders a 0-100 posture score and the findings grouped by category. Every call the scan makes is a read.

It lives in the dashboard Posture tab, under the Ping Identity provider tab. Scan rows carry provider set to ping, which is also the value to filter on in the scans table.

Who can run it. Posture is an Enterprise tab: on the starter and pro plans the dashboard shows an upgrade panel in place of the tab. The plan gate is in the dashboard only. The shield-ping-ispm function enforces something different: the caller must present a valid dashboard session and be an admin member of the tenant in shield_tenant_members.

Console-only. shield-ping-ispm is not in the Cloudflare worker's PUBLIC_FUNCTIONS list, so it has no https://api.passkeybridge.io/v1 route and no API key lane. The dashboard calls it directly with supabase.functions.invoke and your session JWT.

Worker application and dashboard fields

In PingOne. Create a worker application in the environment you want scanned, note its client ID and secret, and give it a role assignment that allows reading the environment's configuration and identity data. The function requests a token with the client_credentials grant and sends no scope parameter, so access is decided entirely by the roles on the worker application.

The region you pick selects both hosts.

RegionToken hostAPI host
NAhttps://auth.pingone.comhttps://api.pingone.com/v1
EUhttps://auth.pingone.euhttps://api.pingone.eu/v1
APhttps://auth.pingone.asiahttps://api.pingone.asia/v1
CAhttps://auth.pingone.cahttps://api.pingone.ca/v1

The token comes from POST {token host}/{environment id}/as/token. The scan then prefetches eleven collections under /environments/{environment id}/ and reuses them: signOnPolicies and each policy's actions, users, groups, populations, applications, passwordPolicies, certificates, identityProviders, riskPolicySets, schemas and branding/themes. Lists ask for 100 per page and follow the HAL _links.next.href link for up to 20 pages or 2,000 items, with themes capped at 5 pages.

A collection the worker application cannot read turns its checks into skipped findings whose evidence carries the HTTP status and the first 300 characters of PingOne's error body, and skipped checks leave the score alone.

In the dashboard. Posture tab, Ping Identity provider tab, expand Ping Identity configuration.

FieldGoes toStored as
Environment IDping_environment_id on shield_tenantsClear text. Must be a UUID.
Regionping_region on shield_tenantsClear text. One of NA, EU, AP or CA.
Client IDping_client_id on shield_tenantsClear text.
Client secretping_client_secret_encrypted on shield_tenantsAES-256-GCM under the server-held SHIELD_ENCRYPTION_KEY.

Press Save. The environment ID, client ID and client secret are required on every save.

{ "action": "save_ping_config", "tenant_id": "<tenant uuid>", "ping_environment_id": "<environment uuid>", "ping_client_id": "<client uuid>", "ping_client_secret": "<secret>", "ping_region": "NA" }

A successful save answers {"ok": true} and writes a save_ping_config audit row.

StatusBody
400ping_environment_id, ping_client_id, and ping_client_secret are all required
400Invalid PingOne Environment ID format (expected UUID)
400Invalid region: X. Valid: NA, EU, AP, CA
500Failed to save provider configuration

Checks by category

25 checks run on every scan, in nine categories.

CategoryChecksWhat they read
authentication6Sign-on policy coverage, the share of login policies carrying an MFA action, FIDO2 or security key in those MFA actions, policy sprawl above 15, identifier-first flows, and whether MFA actions reference a device authentication policy.
applications5Application inventory by protocol, disabled applications, admin-privileged applications, sign-on policy assignment coverage, and SAML assertion signing configuration.
identity_store4User count, the share of disabled users, population segmentation, and schemas with extended attributes.
credential_management3Password policies shorter than 12 characters or without complexity, missing lockout thresholds, and password history below 5.
certificates2Certificates already expired or expiring within 30 days, and the certificate inventory.
governance2Progressive profiling or consent actions, and whether a branding theme carries a logo.
authorization1Group count, passing at 3 or more.
federation1External identity providers configured.
risk1Whether any PingOne Protect risk policy set exists.

There is no admin-role check. The module calls no roles API. Admin-Privileged Application Sprawl is a heuristic over the application list: it counts applications whose access control role type is ADMIN_USERS_ONLY or whose name contains "admin", passing at 2 or fewer and failing above 5. The finding says so in its description.

MFA coverage is per policy. The MFA check compares the number of sign-on policies that contain an MFA action against those that contain a LOGIN action. It passes only at 100 percent, warns at 50 or more, and fails below that. If no policy actions could be fetched at all, the check is skipped instead of failing.

Posture score

The score is a penalty ratio over the checks that actually ran. Every check carries a hard-coded weight and a score_impact: zero when it passes, a negative number when it fails or warns. The function computes, over all non-skipped checks, 100 x (1 - total_penalty / max_penalty), where total_penalty is the sum of the absolute score_impact values and max_penalty is the sum of weight x 2. It rounds it and clamps it to 0-100. Weights run from 1 to 10. Ping's heaviest checks are sign-on policy coverage and MFA enforcement at 10 each, then password policy strength at 7.

There are no severity multipliers. severity (critical, high, medium, low, info) is a label on the finding row and has no effect on the arithmetic.

Skipped checks leave both sides of the ratio, so a check that could not run neither helps nor hurts. If every check is skipped the score is 0.

The tab turns the number into one of four labels.

ScoreLabel in the tab
80-100Strong
60-79Moderate
40-59Needs work
0-39Critical

Credential and result storage

Credentials. Only the client secret is encrypted, with AES-256-GCM under SHIELD_ENCRYPTION_KEY, and it is decrypted inside the function for the length of one scan. Encryption is mandatory: a save fails rather than storing plaintext if the key is missing. The environment ID, client ID and region stay in clear text because the token request needs them, and a migration revokes column-level SELECT on ping_client_secret_encrypted from the authenticated and anon roles.

Findings. Evidence holds counts, percentages and object names, for example the names of weak password policies, the names of applications flagged as admin-level, or certificate expiry counts. PingOne user records are counted inside the function and are not stored.

Scan rows land in shield_posture_scans and one finding row per check in shield_posture_findings. Both tables are tenant-admin read-only under row-level security: insert, update and delete are denied to the authenticated role, and the function writes them with its service-role client.

Audit trail. A completed scan writes a run_ping_posture_scan row to shield_audit_log with the provider, score, check counts and scan id. A failed scan writes the same verb with result set to failure.

Running a scan and retrying failures

Scans run on demand. No cron job calls any ISPM function, so a scan happens when someone presses a button in the Posture tab. Open the tab, select Ping Identity, and press Run scan in the tab header. The button is disabled while a scan is in flight and the tab reloads the scan list when it finishes.

The button invokes the function with your signed-in session.

const { data, error } = await supabase.functions.invoke("shield-ping-ispm", {
  body: { action: "run_scan", tenant_id: "<tenant uuid>" },
});

A completed scan answers with the summary the tab renders.

{
  "scan_id": "7c1f0b2e-...",
  "parent_scan_id": null,
  "posture_score": 68,
  "checks_total": 25,
  "checks_passed": 14,
  "checks_failed": 5,
  "checks_warning": 4,
  "checks_skipped": 2,
  "duration_ms": 8421,
  "partial_failures": []
}

Retry failed checks appears in the Findings header once a completed scan has at least one failed or warning check. It creates a second scan that re-runs only the failed and warning check ids and copies the passed and skipped findings forward, so a skipped check changes only on a full Run scan.

{ "action": "retry_failed_checks", "tenant_id": "<tenant uuid>", "retry_of_scan_id": "<scan uuid>" }
StatusBodyCause
401Missing authorization header or UnauthorizedNo session JWT, or it has expired.
400Invalid JSON bodyThe body did not parse as JSON.
400tenant_id requiredtenant_id was missing from the body.
403Forbidden: tenant admin access requiredThe signed-in user is not an admin member of that tenant.
500Failed to verify tenant accessThe membership lookup itself failed.
400Unknown actionaction was not a save action, run_scan or retry_failed_checks.
400PingOne configuration not found. Configure Environment ID, Client ID, and Client Secret first.No credentials are stored for this tenant.
400PingOne authentication failed: ...PingOne rejected the client credentials. The status and body are included.
400retry_of_scan_id requiredRetry was called without a parent scan id.
404parent scan not foundNo scan row has that id.
403parent scan does not belong to this tenant/providerThe parent scan belongs to another tenant or another provider.
400no failed or warning checks to retryEvery check in the parent scan passed or was skipped.
500Scan failed: ...The scan threw. The scan row is stored with status failed and the thrown message.

Confirm it worked. The score card shows the band, the score out of 100, and the Passed, Failed and Warnings tiles. The card under it shows Scan, Duration, Started and Type, where Type reads Full scan or Retry, N checks. Findings are grouped by category below; expanding one shows its description, Remediation, Evidence JSON, and a line reading Weight, impact and check with that check's numbers and id. The Alerts section in the same tab can post a completed scan to a Slack webhook or to email recipients once its failed-check threshold is met.

Related from the blog