Skip to content

Which of Your Salesforce Connected Apps Use Less Than They're Granted? Ask Your Own Logs

Static scanners tell you what a connected app was granted, not what it uses. Here is how to measure the over-grant per object from your own EventLogFile.

TL;DR

  • Connected-app over-privilege sat at the centre of the 2025-2026 Salesforce data-theft campaigns: OAuth apps authorized with more scope than they use, and integration users with far more object access than the app ever touches.
  • The published fix is least-privilege: cull unused apps, scope tightly. The problem is you cannot safely remove access you cannot see is unused.
  • Static scanners report what was granted. They are blind to what is used.
  • sf audit apps reads the RestApi EventLogFile to see which objects each connected app actually touched, compares that against what its run-as user can reach, and reports the over-grant per object and read/write bit, plus a suggested permission set of exactly what was observed.
  • It is read-only, and honest: RestApi attributes roughly half of API traffic, so "used" is a lower bound, and it says so.

What You'll Learn

  • Why a connected app's real access is two layers, and which one the scanners miss
  • How to measure granted-versus-used per app from a log you already have
  • How to read the output without over-trusting it
  • Where the measurement stops being certain, and how the tool handles that

The Problem

Ask an admin which of their connected apps has more access than it needs, and you usually get a shrug. Not because they do not care, but because the tools only answer half the question.

A typical org runs hundreds of connected apps and OAuth integrations. A security scan will happily tell you that an app holds the full scope, or that an integration user has Modify All Data, or that an app has not logged in for 90 days. That is the granted side, and it is useful. What no static scan tells you is which of those grants the app has ever exercised. So "apply least privilege" becomes advice you cannot action. You do not know which object permission is load-bearing and which is dead weight, so you leave all of it, and the over-grant persists.

That over-grant is exactly what the 2025-2026 wave of Salesforce OAuth attacks fed on. When an attacker rode a trusted connected app or a stolen token, the blast radius was whatever that app's run-as user could reach, which was almost always far more than the app used. The controls that came out of it all say the same thing: least-privilege scopes, cull unused apps, restrict integration users. Every one of those needs a measurement nobody had: what does this app actually do?

Common Questions This Article Answers:

  • How do I tell which connected apps have more access than they use?
  • Where does a connected app's real data access actually come from?
  • Can I generate a least-privilege permission set from real usage?

Quick Answer

sf audit apps ships in the @cclabsnz/sf-audit plugin. Install it, then point the command at your org and give it a window of RestApi logs to read:

sf plugins install @cclabsnz/sf-audit
sf audit apps --target-org myOrg --since 7

For each connected app it reports the objects the app can reach but never touched, the read/write permissions it holds but never exercised, whether its OAuth scope can be narrowed, and a generated permission set granting exactly what it used. Read-only throughout: the permission set is output as data, never deployed. The rest of this article is how it decides that, and where it stops being sure.

A connected app's access is two layers, and the scanners miss the second

Here is the thing most connected-app reviews get wrong. They look at the OAuth scope and stop.

An app's real data reach is the intersection of two things. The first is the OAuth scope: full, api, and the rest. The second, and this is where the over-privilege actually lives, is the permissions of the user the app runs as. A connected app acts as the user who authorized it, or, for a headless integration using the JWT bearer or client-credentials flow, as a designated run-as user. So an app holding the narrow api scope but running as an administrator has full access to your data. The scope looks tidy; the reach is total.

This is why scope-only checks give false comfort. The scope is the door. The run-as user's permission set is every room the door opens onto. To find over-privilege you have to look at the second layer, and compare it against what the app was ever observed doing.

Granted versus used, from a log you already have

The "granted" side is a permission calculation you can do with SOQL: take the app's run-as user, resolve the object permissions their permission sets confer, and you have the ceiling of what the app could touch.

The "used" side is the part people assume needs Shield. It does not. The free RestApi EventLogFile records, for each request, the connected app that made it (CONNECTED_APP_ID), the object (ENTITY_NAME), the HTTP method, and the row count. Group by app and you have the exact set of objects each connected app actually touched, and from the method you can tell reads from writes. Subtract used from granted and you have the over-grant, per object and per verb.

sf audit apps does exactly this. For each app it emits the objects granted-but-never-used, the verbs held-but-never-exercised (write access that only ever did reads, for instance), a full-to-api scope downgrade where no write was ever seen, and a permission set scoped to precisely what the app used. That last output is the useful one: instead of "this app is over-privileged," you get "here is the permission set that grants what it actually needs, based on the last N days."

Resolving app IDs to names a human can read

A finding that says 0H4xx00000000AbC has more access than it uses helps nobody. A connected-app ID in the logs is a 15-character key, and half the time it does not resolve through the object you would expect.

So the tool runs a cascade. It tries AppMenuItem and ConnectedApplication, which resolve most apps, including the standard Salesforce tools like Data Loader and Workbench and your own named integrations. For the IDs those miss, which in practice are often the highest-volume integrations, it correlates against LoginHistory, whose Application field carries friendly names like DataDog or Microsoft Defender. A bundled catalog of well-known app IDs fills in vendors and one-line purposes. And anything that still will not resolve is surfaced loudly, not hidden, because an app you cannot even name that is pulling millions of rows is itself the finding.

Every app also gets a category: Salesforce-standard, org-custom, security-vendor, third-party, or unidentified. That tag is what lets a non-expert read the result correctly. Data Loader holding broad access is expected. An unidentified third-party app holding the same access is a Monday-morning problem.

Where the measurement stops being certain

This is the part a responsible tool has to be loud about, because the failure mode is a confident wrong answer that gets someone to revoke access an app quietly needs.

RestApi attributes roughly half of API traffic to a connected app. The rest is UI-API and session traffic with no app context. So the "used" set is a lower bound, not a complete picture. An app might use an object rarely, or seasonally, or through a path the log does not attribute. Treating "not seen this week" as "safe to revoke" is how you break a quarterly integration.

So the tool refuses to over-claim. Every finding carries its observation window and the attribution rate. Below a soak window it reports usage but suppresses the revoke recommendations. Apps used by many interactive users, where least-privilege is a per-user question rather than a per-app one, are flagged rather than diffed. The output is a lead you confirm and a permission set you review, not a change you apply blind.

Frequently Asked Questions

Q: Does this need Shield or Event Monitoring?

A: No. It reads the free daily RestApi EventLogFile, the same free tier that gives you login and API logs without the add-on. The running user needs the "View Event Log Files" permission.

Q: My app shows almost no usage. Should I revoke its access?

A: Not on that alone. RestApi attributes roughly half of API traffic, so absence of observed usage is a lower bound, not proof the access is unused. Widen the window, confirm the app is not a rare or seasonal integration, and treat the generated permission set as a reviewed proposal, not an automatic change.

Q: The app only has the api scope. Is it fine?

A: Not necessarily. The scope is only the first layer. What the app can actually read is decided by its run-as user's permissions. An api-scoped app running as an administrator has full data access. Audit the run-as user, not just the scope string.

Q: Does it change anything in my org?

A: No. It is read-only: SOQL selects and a REST GET of the log file. The suggested least-privilege permission set is emitted as data for you to review and deploy yourself, never applied.

Q: What about SOAP API traffic?

A: The SOAP API log does not carry a connected-app ID, so SOAP-only apps cannot be attributed the same way. The tool notes this rather than wrongly reporting such an app as dormant.

Key Takeaways

  • Access is two layers. A connected app's real reach is its OAuth scope intersected with its run-as user's permissions, and the over-privilege almost always lives in the second layer that scope-only checks ignore.
  • Used is measurable for free. The RestApi EventLogFile attributes API calls to connected apps and objects, so you can compute granted-versus-used without Shield.
  • The output is a permission set, not a scolding. The valuable result is a least-privilege permission set scoped to observed usage, ready to review.
  • Used is a lower bound. Attribution is partial, so trust the soak window and the attribution rate, and confirm before you revoke.

What's Next?

Recommended Reading:

Action Items:

  1. Confirm your running user has "View Event Log Files" and that your edition exposes free daily logs.
  2. Run sf audit apps --target-org <alias> --since 7 and read the highest-volume apps first.
  3. For each over-grant, review the generated permission set, widen the window if usage looks thin, then tighten the run-as user and the scope.

Tried it on your own org, or want it to account for something it misses (profile-based permissions and OAuth-scope reads are on the roadmap)? Leave a comment below.

Resources & References