CyberArk Identity Security Posture Management (ISPM)
Console-only posture scanning for a CyberArk Identity tenant: 25 weighted Identity Platform checks in six categories, run from the dashboard Posture tab and scored 0-100 by a credit formula unique to this provider.
Overview
PasskeyBridge reads a CyberArk Identity tenant through the Identity Platform REST API, runs 25 checks against its users, roles, policies, federated applications, admin accounts, directory services, MFA profiles and audit summary, 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.
This module scores differently from the other six providers. It awards credit for checks that pass instead of subtracting penalties, and skipped checks cost you points. The Posture score section below has the formula.
It lives in the dashboard Posture tab, under the CyberArk Identity provider tab. Scan rows carry provider set to cyberark.
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-cyberark-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-cyberark-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.
Confidential client and dashboard fields
In CyberArk Identity. Create an OAuth2 confidential client in your tenant and note its client ID and secret. The function requests a token from POST https://YOUR-TENANT.id.cyberark.cloud/oauth2/platformtoken with the client_credentials grant and scope=all, then POSTs to these endpoints with the bearer token:
/CDirectoryService/GetUsers, paginated at 200 rows for up to 10 pages/Roles/GetRolesand/ServerManage/GetAllAdminAccounts/Core/GetPoliciesand/Core/GetMFAProfile/SamlManage/GetApps/CDirectoryService/GetDirectoryServices/Reports/GetAuditSummary, asking for 50 rows
The client needs read access to all eight. A dataset that fails to load turns every check that depends on it into a skipped finding naming the dataset, and on this provider a skipped check lowers the score.
The tenant URL is checked before any request leaves: it must resolve to a public https host, and anything else is refused.
In the dashboard. Posture tab, CyberArk Identity provider tab, expand CyberArk Identity configuration.
| Field | Goes to | Stored as |
|---|---|---|
| CyberArk URL | cyberark_tenant_url on shield_tenants | Clear text. Trailing slashes are stripped and https:// is added when missing. |
| Client ID | cyberark_client_id on shield_tenants | Clear text. |
| Client secret | cyberark_client_secret_encrypted on shield_tenants | AES-256-GCM under the server-held SHIELD_ENCRYPTION_KEY. |
Press Save. All three fields are required on every save.
{ "action": "save_cyberark_config", "tenant_id": "<tenant uuid>", "cyberark_tenant_url": "abc1234.id.cyberark.cloud", "cyberark_client_id": "posture-scanner", "cyberark_client_secret": "<secret>" }A successful save answers {"ok": true} and writes a cyberark_config_saved audit row. A missing field answers 400 with Tenant URL, Client ID, and Client Secret are all required.
Checks by category
25 checks run on every scan, in six categories.
| Category | Checks | What they read |
|---|---|---|
| authentication | 11 | MFA profiles and the share of users with MFA, password policy strength, adaptive authentication, session timeout, self-service password reset, FIDO2 or WebAuthn support, custom authentication rules, IP or geo restrictions, certificate-based authentication, and the account lockout threshold. |
| lifecycle | 4 | Accounts with no sign-in for 90 days, locked or disabled accounts, active directory synchronisation, and automated provisioning. |
| pam | 4 | The privileged account ratio, break-glass accounts, the service account inventory, and privileged session recording. |
| rbac | 3 | Administrative account sprawl, role-to-user ratio, and the share of users assigned to roles. |
| federation | 2 | Federated application coverage, and SAML apps still signing with SHA-1. |
| observability | 1 | Whether the audit summary returns recent events. |
Many CyberArk checks are keyword heuristics. CyberArk returns policies as generic objects, so these checks decide by matching words in a policy name or in the serialised policy JSON rather than reading a typed setting: Password Policy Strength, Adaptive / Risk-Based Authentication, Session Timeout Policy, Self-Service Password Reset, Custom Authentication Rules, IP / Geo-Based Access Restriction, Certificate-Based Authentication, Automated Provisioning, Privileged Session Recording and Account Lockout Policy. Administrative Account Sprawl and Privileged Account Ratio fall back to role names containing "admin" when the admin accounts endpoint returns nothing, and Emergency / Break-Glass Account matches user names containing "break", "emergency" or "glass". Treat those findings as prompts to verify in the CyberArk console.
Four checks report a fixed status rather than a measurement. Self-Service Password Reset, Certificate-Based Authentication and Custom Authentication Rules pass when a matching policy is found and record skipped otherwise. Locked / Disabled Accounts Present passes when locked accounts exist and warns when none are. Because this provider scores by status, a skipped result earns no credit.
Posture score
CyberArk Identity is the one module that scores by credit. The function adds up the weight of every check that passed plus half the weight of every check that warned, divides by the total weight of all 25 checks, multiplies by 100 and rounds.
const earned = checks.reduce((s, c) =>
c.status === "pass" ? s + c.weight :
c.status === "warning" ? s + c.weight * 0.5 : s, 0);
const score = Math.round((earned / totalWeight) * 100);Two consequences follow. A failed check and a skipped check are worth the same here: nothing. And because the denominator is every check's weight, a permissions problem that skips half the scan drives the score down, where on the other six providers a skipped check drops out of the calculation entirely. If a CyberArk score looks low, check the skipped count before reading it as a posture problem.
Weights run from 2 to 10; the heaviest check is MFA Profile Configuration at 10, then MFA Enforcement Coverage at 8. score_impact is still stored on each finding and the tab still displays it, and on this provider it does not feed the score.
The tab turns the number into one of four labels, the same as every other provider.
| Score | Label in the tab |
|---|---|
| 80-100 | Strong |
| 60-79 | Moderate |
| 40-59 | Needs work |
| 0-39 | Critical |
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 tenant URL and client ID stay in clear text because the token request needs them, and a migration revokes column-level SELECT on cyberark_client_secret_encrypted from the authenticated and anon roles.
Findings. Evidence holds counts, ratios and booleans, for example admin_count, stale_count or whether a keyword was detected. CyberArk 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. This module keeps its own verbs: cyberark_config_saved when credentials are saved and cyberark_ispm_scan_completed for a scan, rather than the run_cyberark_posture_scan the shared runner would have used. A failed scan writes the same scan 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 CyberArk 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-cyberark-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>" }| Status | Body | Cause |
|---|---|---|
| 401 | Missing authorization header or Unauthorized | No session JWT, or it has expired. |
| 400 | Invalid JSON body | The body did not parse as JSON. |
| 400 | tenant_id required | tenant_id was missing from the body. |
| 403 | Forbidden: tenant admin access required | The signed-in user is not an admin member of that tenant. |
| 500 | Failed to verify tenant access | The membership lookup itself failed. |
| 400 | Unknown action | action was not a save action, run_scan or retry_failed_checks. |
| 400 | CyberArk Identity is not configured. Save credentials first. | No credentials are stored for this tenant. |
| 404 | Tenant not found | The tenant row could not be read. |
| 400 | CyberArk authentication failed: ... | The platform token request was rejected. The status and body are included. |
| 400 | retry_of_scan_id required | Retry was called without a parent scan id. |
| 404 | parent scan not found | No scan row has that id. |
| 403 | parent scan does not belong to this tenant/provider | The parent scan belongs to another tenant or another provider. |
| 400 | no failed or warning checks to retry | Every check in the parent scan passed or was skipped. |
| 500 | Scan 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
- Account Recovery Is the Front Door: Carrier-State Checks in Credit Union and Community Bank Recovery Flowssecurity · 11 min read
- Roaming Steering and Identity: When the HPLMN You Trust Hands You to a VPLMN You Don'tsecurity · 20 min read
- Network Slicing in 5G Standalone and the Birth of Per-Slice Identity Posturentn · 15 min read