Back to CallCamIT

Security assessment

The adversarial review of an autonomous agent with administrative reach, and the controls built against it — including what is still weak.

CallCamIT is, by design, one of the most dangerous kinds of software you can put on a corporate estate: an autonomous agent with local administrative reach on every desktop and privileged access to Entra ID. This document is the adversarial review of that design and the controls implemented against it.

1. Threat model — how an attacker would steal data through CallCamIT

Assume the attacker's goal is bulk exfiltration of personal data, credentials, or intellectual property from a UK enterprise. CallCamIT is an attractive target precisely because it legitimately holds broad access.

# Attack path Why it's attractive Control implemented
T1 Steal the Azure app secret from disk/config and use Cam's directory roles from anywhere One secret = tenant-wide helpdesk privilege Secret encrypted with Windows DPAPI (per-user + app entropy) in config.json; never written to logs; config.json and the data directory ACL'd to the owning user; env-var override supported so it can live in a vault instead
T2 Read the ticket database — descriptions and event logs are full of names, hostnames, credentials users pasted in One file, huge PII yield AES-GCM field encryption at rest for descriptions, resolutions, event content, issue and escalation bodies; key held in DPAPI, never on disk in the clear; DB file ACL'd to the user
T3 Prompt injection — a malicious file, event-log entry, web page or ticket text tells Cam to run an exfiltration command LLM agents follow text they read Tool scope gating is enforced in code, not by the prompt: model output can never grant itself local or Azure scope; local tools require a per-ticket human consent flag, or standing access an administrator switched on (the person's per-ticket revoke still overrides it); the shell is an allowlist: only known read-only commands run unasked, anything else waits for a person to approve the literal command, and a list of catastrophic operations is refused in every mode; guardrails block destructive Graph/ARM paths regardless of what the model asks
T4 Exfiltrate via the internet tool — encode stolen data into a URL or POST it out The obvious egress channel Research is GET-only (no POST/PUT/PATCH/DELETE exists in the tool), restricted to an allowlist of vendor documentation and support domains, the NCSC, the ICO, NIST, MITRE and three technical question-and-answer sites, and a redirect is checked against the allowlist before it is followed. The search phrase is scrubbed of email addresses, IPv4, IPv6 and MAC addresses, GUIDs, UNC paths, UK postcodes, UK phone numbers, user folder names, labelled secrets and long opaque strings before it is sent. The address of a page Cam reads (web_fetch_doc) is checked against the same allowlist and is never rewritten: a query string over 200 characters is refused, a query addressed to the search engine is refused (a search goes out as the scrubbed phrase or not at all), and an address carrying any of those identifiers, in the forms these patterns recognise, is refused rather than fetched. There are two allowances. In the host and path of any address, a long code counts only as 32 or more characters with no hyphen that hold both a letter and a digit, so a long hyphenated title, a code split by hyphens or a letters-only page name goes out as written. And in an address the search itself returned, a GUID or a long code outside the query string is read as the page's own name. Every request is written to the audit trail as an egress event and counted on the Security tab
T4b Exfiltrate by email — Cam holds Mail.Send as an application permission, so it can send as any mailbox in the tenant A second egress channel beside the research tool, and one that can also sign invoice fraud with a real director's name az_send_mail refuses any recipient outside the organisation's own verified domains (label-boundary matched, so evilcontoso.com is not contoso.com), splits the recipient string before validating so a smuggled second address cannot ride along on a valid first one, refuses everything when the copy is not registered to a tenant, and records every send as an egress event on the ticket. Found by audit 2026-08-31: until then this tool had no recipient restriction, no audit and no confirmation
T5 Abuse Cam to escalate privilege — have it add an attacker to Global Admins or grant an Azure role Instant tenant takeover guardrails.py hard-blocks roleManagement/directory/roleAssignments, directoryRoles/*/members, Global Admin role template in any body, Conditional Access writes, policies/* writes and application deletion. Cam is deliberately never assigned Global Administrator during self-setup
T5b Escalate privilege through a group — add the attacker to a role-assignable group instead of to the role itself A role-assignable group hands its directory role to every member, and adding one is an ordinary groups/{id}/members/$ref POST that touches neither roleAssignments nor directoryRoles/*/members az_group_membership reads isAssignableToRole before any add and refuses the membership if the group carries a role — or if the group cannot be read at all, because "I could not tell whether this grants Global Administrator" is not a yes. Found by audit 2026-08-31
T6 Mass destruction to cover tracks or extort Ransomware-adjacent Blocked in CallCamIT's Microsoft Graph and Azure Resource Manager tools: deleting the organisation, domains, subscriptions, resource groups, SQL/Cosmos/Postgres/MySQL/storage/Key Vault resources, sites, drives, mail folders, teams, backups, and Cam's own app registration, and these ways of deleting something for good: emptying Entra ID's recycle bin (DELETE directory/deletedItems) or a SharePoint site's, permanentDelete wherever Graph offers it (a file, a message, a mail folder, a contact, an event), deleting a device (Microsoft's device soft delete is a preview, and a device it does not cover is gone at once with its BitLocker recovery keys), and deleting through the untyped directoryObjects/{id} path, which cannot say whether it names a user, a group or an application. Those Graph tools also refuse to delete a user or a group on a ticket; Admin Cam deletes only accounts and Microsoft 365 groups, both recoverable for 30 days, and no more than three accounts in a session
T7 Tamper with the audit trail after acting Hides the breach Events are chained with HMAC-SHA256 under the DPAPI-protected key (prev_hash → hash), and the count and head hash are anchored in a file outside the database so deletion from the end is visible too. Any edit or deletion breaks the chain; the Security tab verifies it on demand and reports the first bad event id
T8 Read other people's tickets from a normal user account Lateral PII access Tickets are keyed to a resolved domain identity (user_key); the user window queries only that key; selecting another user's ticket id is refused in code. The whole ticketing system, analytics and controls live in a separate PIN-gated dashboard
T9 Brute-force the controller PIN Full control of Cam PBKDF2-SHA256, 600,000 rounds (OWASP 2023), per-PIN random salt, a minimum length and a common-PIN blocklist; 5 attempts then a 5-minute lockout; the field clears on failure
T10 Harvest secrets from the chat/log — Cam echoes a temp password, BitLocker key or card number a user pasted Credentials in plaintext logs Redaction before write: passwords, tokens, JWTs, API keys, card numbers (PAN), UK NI numbers, IBANs, private keys and BitLocker recovery keys are replaced with placeholders in every event and issue record; the system prompt also forbids echoing them
T11 Use Cam as a lateral movement tool on servers Domain-wide reach Cam restarts a stopped service once and reports; it does not reconfigure roles, edit databases, fail over clusters, or reboot production servers — all escalate. AD runbooks explicitly forbid touching privileged groups
T12 Silent long-term access via a rogue exclusion or weakened policy Persistence Cam is instructed and gated never to weaken AppLocker/WDAC, Conditional Access, or add broad Defender exclusions; those escalate to a human
T13 Injection through the screen itself — a support page, a spoofed dialog or a crafted document filename tells Cam, in a window it is reading, to type a password out or run a command Cam now reads windows, so any text on screen is model input Window text is screened by scan_screen_text (24 instruction patterns) before it reaches the model, the remainder is wrapped in an explicit untrusted-content envelope, the system prompt states that screen content is information and never instruction, and the attempt is written to the ticket. Even a successful persuasion hits the second tier: PermissionGate.check screens the action, so a typed credential or a destructive command is refused or halted on its own merits
T14 Abuse desktop control directly — drive a password manager, a banking site or a checkout with the user's own session Cam's clicks carry the user's authenticated context Prohibited categories are refused in every mode and cannot be unlocked by a remembered rule: purchases and payments, card/bank/identity numbers, wiping a drive, deleting a user, mailbox or tenant, boot and registry hives, disabling AV or the firewall, CAPTCHAs, moving money. Password managers, remote desktop, BitLocker tools and the credential store are on an app blocklist. Protected actions (deletions, credential entry, elevation, registry, services, network, installs, sending, form submits) always require a human nod and can never be remembered
T15a Get Admin Cam to delete accounts as a way to cause damage or cover tracks Account deletion is the highest-value destructive action in a tenant Two independent gates: the controller PIN and a live Entra ID directory role, so authority is revocable in your tenant rather than in a local list. Every irreversible action needs the admin to type the exact target — the model cannot confirm on their behalf. Deletion is soft only: the account lands in the 30-day recycle bin and DELETE directory/deletedItems (the permanent purge) is in the never-list, so the undo cannot be taken away. Privileged, break-glass, Cam-owned and self accounts are refused before anyone is asked to confirm. Bulk destruction capped at 3 accounts per Admin Cam session (CC-4123). Every action is a admin_action event in the hash chain, attributed to the named admin
T15b Reach admin tools from a normal ticket — prompt injection in ticket text, a crafted screen, or the model simply deciding it should The agent already holds Azure privilege; if account tools were merely discouraged, text could reach them Admin tools live in SCOPE_ADMIN, which is only added to a call's scope set while a session is open — the check is in ToolRegistry.call, not the prompt. The escape-hatch Graph tool is independently gated: guardrails.check raises NeedsAdmin for user/group creation and deletion unless the caller is elevated, and only Admin Cam's operations (admin/operations.py) send a Graph request with elevated=True (the troubleshooter's admin-limits and self-attack checks call guardrails.check elevated, but send nothing). Scope is recomputed every agent turn, so an expired session loses its powers mid-conversation
T15c A stolen or shoulder-surfed PIN on an offline machine Directory checks can't run when the tenant is unreachable, which is when a stolen PIN is most useful A session that cannot reach Entra ID opens degraded: it can read the directory and changes nothing (no create, change or delete) until the directory can confirm who is driving, and each change it refuses carries status code CC-4122. "Could not check" is never treated as "yes"
T15d Hide a directory role behind a page boundary so the "never delete an administrator" check does not see it memberOf returns groups and directory roles mixed together and Graph pages it, so a senior account in many distribution lists has its role assignment on page two — and the busiest directory members are the ones most worth protecting Every memberOf read that decides an authorisation question is paged=True: GraphClient.is_privileged, AdminOperations._roles_of, AdminOperations.offboard_user and AdminSession._verify_directory. A source test asserts all four, and test_redteam.py hides a Global Administrator role behind a page boundary and asserts the account is still refused. Found by audit 2026-08-31: _roles_of was unpaged, so a Helpdesk Administrator could have deleted a Global Administrator, and offboard_user left every membership past the first page in place — a leaver keeping access
T15 A standing "always allow" grant accumulated during one repair is reused later for something else Permission creep Grants are scoped to an app, a tool or a command prefix — never blanket — are listed with who granted them and when in Controller → Permissions, are individually and collectively revocable, and never cover a protected or prohibited category. The troubleshooter warns once more than 25 accumulate

2. Controls summary

Identity & access

Consent & lawful access (Computer Misuse Act 1990)

Data protection (UK GDPR / DPA 2018)

Operational

2b. Microsoft API conformance review (2026-08-30)

Reviewed against current Microsoft Learn documentation for app-only Graph and ARM access. Implemented since:

Documented requirement Source Status
client-request-id GUID on every request, and logged Graph best practices Now sent and retained, with the server's request-id, so a support case starts with the identifier Microsoft looks up
Always handle 429; honour Retry-After Graph throttling Now retried on 429/503/504 using the server's own interval, capped at 90 s, 3 attempts. Previously a throttle surfaced to the user as a failed ticket
Declare cp1 and handle claims challenges CAE for workload identities Now enable_cae=True, and a 401 carrying a challenge re-authenticates instead of failing. Without this, disabling the service principal left already-issued tokens working for up to 24 hours
Follow @odata.nextLink, treat it as opaque Graph best practices Now paged on every collection read, bounded at 40 pages. is_privileged() in particular could previously miss a directory role that fell on page two — and that function decides whether Cam may reset a password
$batch limit of 20 JSON batching Now enforced in the guardrails with an explanatory refusal
/me is delegated-only Auth basics Now refused — there is no signed-in user on an app-only token
TLS 1.2 minimum Graph best practices Mounted on the session every Graph and ARM call goes through (2026-09-21): GraphClient sends through a requests.Session whose HTTPS adapter hands the connection pool, and any proxy manager, an SSL context with minimum_version = TLSv1_2. Until then the context was built, floored and discarded while the calls went through a bare requests.request, so the floor was OpenSSL's default in fact. tests/test_graph_rules.py reads the minimum version off the pool, and c_graph_conformance reports it
Never parse the access token MSAL token caching Never did; now covered by a test
Least privilege; remove reducible permissions Least-privileged access Permission set reduced. User.ReadWrite.All split into User.Read.All + User.EnableDisableAccount.All + the password and licence permissions it already held; Group.ReadWrite.All reduced to Group.Read.All + GroupMember.ReadWrite.All. Directory.ReadWrite.All, Application.ReadWrite.All and RoleManagement.ReadWrite.Directory were already absent and a test now keeps them out

Found in the tool-coverage review (2026-08-30) and fixed: the app requested DeviceManagementManagedDevices.PrivilegedOperations.All — which permits remote wipe, retire and passcode reset — and nothing used it. It was the most dangerous permission in the manifest and it was there for no reason. Removed from the request, and the destructive Intune endpoints are now in the NEVER guardrails as well, because a rule that depends on a permission never being granted is one grant away from being no rule at all. The test that should have caught this had passed: it looked for the permission's API name anywhere in the source and found it in a runbook's prose, so "justified" now means a tool that actually calls it.

Found during the Microsoft review and fixed: an API-version prefix defeated every directory guardrail. The rules are written against normalised paths like organization, and the canonicaliser stripped a version segment only from absolute URLs — so DELETE v1.0/organization/{id}, which is deleting the customer's tenant, matched no rule at all. Now stripped repeatedly (v1.0/, beta/, and combinations) before any rule is applied, with tests across every rule.

Found by the 2026-09-21 review and fixed: a dot segment did the same from the other side. The HTTP client resolves . and .. before the bytes leave (and sends %2e%2e as a literal .. for the server to collapse), and the canonicaliser never did, so DELETE users/x/../../organization/abc was judged as a users path and sent as the deletion of the organization, and PATCH users/ordinary/../../users/ceo had the directory asked about the wrong account. A path with a . or .. segment is now refused outright, direct and inside a $batch, because no Graph or ARM resource is spelt with one; the wire URL is still built from the path as given (the normalised form drops the query string), and tests/test_redteam.py proves per accepted spelling that the URL on the wire normalises to the path the rules saw. The same review found that minting a credential was not a takeover shape: a Temporary Access Pass, a new phone or email method, or authentication/methods/{id}/resetPassword for an administrator passed both the name check (which counted an MFA change on DELETE only) and the directory lookup. Any write under users/{x}/authentication/ is a takeover shape now, and every shape is read off the normalised path, so authentic%61tion/ no longer reaches the directory lookup as nobody.

3. Residual risks (accept, mitigate, or decide)

  1. A compromised controller account can pause/resume Cam and change configuration. Mitigate by restricting who knows the PIN, and by keeping the Windows account itself protected with MFA/Windows Hello. The audit chain still records everything they do.

  2. Local admin on the endpoint can read the DPAPI-protected key as that user. This is inherent to any local secret store; full mitigation is a hardware-backed store (TPM/Windows Hello for Business) — recommended next step.

  3. Admin Cam concentrates account-lifecycle power in one console. That is the point — it makes joiners and leavers fast and consistent — but it means the controller PIN plus one directory role is enough to disable someone's access. The compensating controls are that a deleted account or Microsoft 365 group is recoverable for 30 days (Admin Cam refuses to delete any other kind of group, security and distribution groups among them, because Microsoft may delete those permanently), CallCamIT's Microsoft Graph tools refuse the permanent deletions Graph offers at every level (emptying Entra ID's or a SharePoint site's recycle bin, permanentDelete, deleting a device, and deleting through the untyped directoryObjects path), privileged and break-glass accounts are untouchable, and every action is attributable in the hash chain. Reviewing Controller → Admin actions is the operational control, and restricting who holds User Administrator is the preventive one.

  4. The Azure app secret is a bearer credential. Rotate it on the schedule your policy requires (the bootstrap issues a 2-year secret); a certificate credential or Managed Identity would be stronger and is the recommended upgrade.

  5. The local LLM can still be wrong. Guardrails prevent catastrophe, not mistakes. The verify phase and the audit trail are the compensating controls.

  6. Desktop control is as trustworthy as the classifier. The prohibited and protected lists are pattern-based, so a genuinely novel phrasing could in principle be classified as routine. Three things bound the damage: protected categories are deliberately broad and always ask; the destructive Windows and Graph paths are also blocked by guardrails.py and the shell's refusal list, independently of this gate; and the desktop_limits troubleshooter check re-runs real refusals rather than trusting the list. Reviewing the audit trail for permission events is the operational control.

  7. The audit trail can now leave the endpoint (siem.py, cfg.siem_forwarding, off by default). Added 2026-09-01. The hash chain proves the local record is intact to somebody holding it; an administrator on the machine can delete the database, and a chain that no longer exists cannot report its own absence. Forwarding writes a summary of each audit event to the Windows Application log, which an enterprise SIEM already collects — no agent, no outbound connection, no credential, and it works on an air-gapped estate. Ticket content is not forwarded: what leaves is the sequence number, the hash, who acted, on which ticket, and what kind of event. That is enough to notice events stopping or a sequence looking wrong, and not enough to read anybody's support history — which matters because a SIEM is often run by a third party. A broken chain is reported this way specifically, since the endpoint cannot be trusted to report that its own record was tampered with. Off by default because it is a new recipient of personal data and that is the controller's decision; c_siem_forwarding warns while it is off. Wired 2026-09-28: until then nothing but the troubleshooter's audit check called the forwarder, and its event ids (1000 to 1900) were outside eventcreate's 1 to 1000. Every committed event of a forwarded kind is now queued from TicketStore.on_commit to a background sender, under source CallCamIT with ids 100 (audit), 110 (account action), 120 (permission), 130 (security: refusals, estate stops, egress, pauses, re-anchors, restores, erasures, failed sign-ins, and the Controller's PIN, settings, rights, export and purge actions) and 190 (broken chain); the all-users installer registers the source, and the troubleshooter reports OK only after a real send.

  8. Physical access to an unlocked machine bypasses the user-isolation model. Standard screen-lock policy applies.

  9. The Azure admin profile could authenticate with a client secret held on the endpoint — and Microsoft documentation says not to. Open. Delegated sign-in exists and is the pushed default, but a machine on that default does no Microsoft work: build_agent only constructs a Graph client when a client secret is present, so every machine that does Microsoft work today holds one, DPAPI-protected in the user's profile, and delegated sign-in has not yet fetched a token from a real tenant. The remediation is designed and not built: the customer's server edition becomes the one confidential client, and the desks ask it over the transport that already exists. The design document is available on request. The two delegated code paths that do exist, me/sendMail in az_send_mail and GET me in signed_in_upn, are refused by the product's own /me guardrail in the real client and pass only in tests whose fake skips the guardrails (noted 2026-09-21), so delegated mode has never made a Graph call. Until it is built, put the Azure profile on the administrators' desks and the server only; the packages already cannot put it on a user desk. VENDOR-ASSESSMENT.md §5 says the same. The history below is kept because the residual case is real — the server edition still needs app-only auth for unattended work, and c_azure_posture reports which mode a machine is in. Microsoft's wording is categorical and names this exact shape:

    "Never publish that credential in your source code, embed it in web pages, or use it in a widely distributed native application." — OAuth 2.0 client credentials flow

    "If an application is used only as a public or installed client (for example, mobile or desktop apps that are installed on the end user machine), make sure that there are no credentials specified on the application object." — Security best practices for application properties

    "Using application permissions for interactive scenarios can put your application at compliance and security risk. It can inadvertently elevate a user's privileges to access data, bypassing policies configured by an administrator." — Graph best practices

    What is better than the worst case: the secret is not shipped in the build. azure_bootstrap creates a per-tenant app registration during install, so each customer holds their own credential, and it is stored DPAPI-protected under the machine's data folder with the ACLs in §2. There is no CallCamIT master key and a breach of one customer does not touch another.

    What remains true anyway, and is the honest statement of the risk: a local administrator on any endpoint can read that credential as that user, and it carries the app's tenant-wide privileges with no user attribution. Concretely — service principal sign-in logs record the app and an IP address, never the person; Conditional Access on the human does not apply, because there is no human in the token; the token is a pure bearer token with no device binding, so Token Protection cannot cover it; and every installation shares one service principal, which flattens ID Protection's behavioural detections and puts the whole estate in a single Graph and ARM throttling bucket. Guardrails bound what the credential can do — 40 operations refused at every level, privileged accounts untouchable, account lifecycle behind a separate PIN — but they run in the same process, so they bound Cam's misuse of it, not an attacker's.

    The documented remediation is not "protect the secret better". It is to make the desktop a credential-free public client (auth code + PKCE, delegated permissions, the signed-in administrator's own rights and their own Conditional Access) and to move genuinely app-only work behind a server-side confidential client authenticating with a managed identity. That is a product decision with real costs — it needs a hosted component, which cuts against the on-premises, air-gap-friendly positioning this product is sold on — so it is recorded here as a decision to take rather than presented as done. Interim mitigations, in order of value: prefer delegated sign-in for Admin Cam so the administrator's own token is used; rotate the secret on the tenant's policy and move to a certificate credential; restrict which machines get an Azure profile at all rather than deploying it estate-wide; and send service principal sign-in logs to a SIEM, since Entra ID Free keeps only 7 days.

    What exists, and what is designed but not yet working. cfg.azure.auth_mode defaults to delegated, and deployment.build_package pushes delegated to every desk and reserves app for the server edition, the one machine with work that genuinely happens while nobody is signed in (publishing the estate ticket feed). No policy file can carry the secret itself (azure.client_secret is in config.NEVER_MANAGED). Delegated mode is designed to make the desktop a public client: authorisation code plus PKCE in the administrator's own browser, no credential stored, Graph calls carrying their identity, their own Conditional Access applying, and the tenant's sign-in log naming a person instead of an application. That is not what happens today. As the start of this item says, build_agent builds a Graph client only when a client secret is present, so a machine on the delegated default with no secret does no Microsoft work at all, delegated sign-in has not yet made a Graph call, and every machine that does Microsoft work today holds a client secret, whichever mode it is set to. Admin Cam records acting_as on every sign-in, and when an administrator's actions will not name them the sign-in notice carries the warning (AdminSession.posture_warning), which the console shows in its transcript once the administrator is signed in (since 2026-09-21; before that day the warning had no caller and only the audit line existed).

    What is left. Until the remediation above is built, the client secret is on every machine that does Microsoft work, so keep the Azure profile to the administrators' desks and the server. Once it is built, the server edition will still hold a Microsoft credential, because its unattended work cannot be done as a person (the design gives it a certificate rather than a shared secret). The mitigations are the ones already listed: rotate on your policy, prefer a certificate credential over a shared secret, and forward service principal sign-in logs to a SIEM, since Entra ID Free keeps only seven days. Mail.Send as an application permission (HARDENING.md item 8) is unchanged, and every machine that holds the secret can use it.

  10. A development build exists that skips the licence check (CALLCAMIT_DEV=1). Deliberately not a security risk in a deployed estate, for a reason worth stating explicitly rather than assuming: the check that disqualifies it — the build being frozen — is the first thing evaluated, is a property of how the binary was produced, and cannot be reached by anything outside the process. CallCamIT.exe therefore refuses development mode before it reads any environment variable, config value, registry key or command-line flag, so no user, administrator or attacker on a customer machine can switch it on. It additionally requires the source tree (tests/, requirements.txt) and an explicit opt-in. It relaxes licensing only: every guardrail, the shell classifier, the permission gate, the controller PIN and the compliance routing are unchanged, and tests/test_devbuild.py asserts that by comparing their behaviour with the bypass on and off. A dev build is loudly labelled — window title, tray, Controller, troubleshooter report header — and records dev_build in the hash chain, so a dev run cannot later be mistaken for a licensed one. The residual point that is real: licensing was never a security boundary (it is a commercial control: the Ed25519 signature, made with a key that never ships, stops a customer editing their own expiry date), so this changes the commercial position not at all and the security position not at all.

  11. The PowerShell refusals are a denylist. tools/shell.py refuses the destructive and protection-off commands by how they are written, and tests/test_redteam.py pins every form it covers. PowerShell accepts more spellings than any list holds, so a command written another way is not refused: it is judged like any other change. What stands behind the list is the working mode. A command Cam wrote itself is shown to a person before it runs in the manual and auto modes (in auto, until somebody chooses "always allow" for it), and runs unasked in skip, which an administrator has to choose. Treat skip as a decision to trust the model's commands, and keep it for machines where that is acceptable. The Microsoft 365 and Azure refusals are not affected: Cam's own Graph and Resource Manager tools judge the path they send, whatever the model wrote.

4. Deployment recommendations (enterprise)

This document describes implemented technical controls. It is not legal advice; have your DPO or legal counsel confirm your obligations under UK GDPR, the DPA 2018 and any sector rules before deployment.