sf-audit v1.0: New Checks, Configurable Scoring, and Externalized Queries

The first version of sf audit security covered the obvious risk: who has ModifyAllData, what your password policies look like, which connected apps have no IP restrictions. That catches the visible surface.

What it missed was the indirect layer — automation that bypasses sharing, integrations that embed credentials in code, guest profiles with write access to sensitive objects, and public groups quietly over-sharing records. This update closes those gaps.


What's New at a Glance

Area Before After
Security checks 22 23
Scoring model Fixed weights Configurable via --scoring-config
SOQL / Tooling queries Inline in code Externalized to config/queries/
High-risk findings Username only Username + Profile + clickable org link

The Four New Threat Surfaces

The additions share a common theme: they target the automation and integration layer, not just org configuration. Flows, Apex integrations, Experience Sites, and sharing rules all operate outside the standard permissions model — and that is exactly where risk accumulates without anyone noticing.

Flows Running Without Sharing

Salesforce Flows have a Run As setting that most teams set and forget. When an autolaunched Flow is configured to run with SystemModeWithoutSharing, it bypasses all record-level security — it sees and can modify every record in the org regardless of the triggering user's permissions.

The check queries active Flow versions via the Tooling API and inspects RunInMode. A null value on an autolaunched Flow is treated as SystemModeWithoutSharing, which matches Salesforce's own default behaviour for that process type.

  • Autolaunched Flows with SystemModeWithoutSharingHIGH. They run silently on record changes or schedules with no user in the loop.
  • Screen Flows with SystemModeWithoutSharingMEDIUM. User interaction reduces the blast radius, but the data exposure is still real.

Remediation: In Flow Builder, change Run As to System Context With Sharing or User or System Context where the flow logic permits it.


Hardcoded Credentials in Apex

The check scans every non-namespaced Apex class for credential patterns, stripping block comments first to reduce false positives. Classes over 100,000 characters are skipped and noted in the output.

HIGH patterns:

  • Hardcoded Bearer tokens: Bearer <token>
  • Hardcoded Basic auth: Basic <base64>
  • API key assignments: api_key = "..." or apiKey: "..."

MEDIUM pattern — raw endpoint URLs that are not covered by a Named Credential or Remote Site Setting:

// Flagged: raw endpoint not registered anywhere
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.example.com/v1/data');

This cross-reference is what makes the check non-trivial. Named Credential endpoints and Remote Site URLs are cached from earlier checks in the run and compared against every setEndpoint() call. If the base URL is already registered, it is not flagged — the call is managed. If it is not, that is an untracked integration.


Guest User Access

Guest users are unauthenticated. They represent every anonymous visitor to an Experience Site. The check only runs if there are live sites — if your org has no active Experience Cloud sites, it exits cleanly.

For each live site, the check resolves the guest profile and inspects object permissions for Account, Contact, Case, and Lead:

  • Create or Edit permissions on any of these objects → HIGH
  • Read permissionsMEDIUM

If Health Cloud is installed (detected via the HealthCloudGA package licence), the sensitive object list extends to clinical records: EhrCondition, EhrMedication, EhrProcedure, and EhrObservation. Guest read access to a patient's medication list is a different category of problem from guest read access to Accounts.

The check also inspects sharing rules on the standard share tables (AccountShare, CaseShare, ContactShare, OpportunityShare) for rules that grant access to guest users directly.


Public Group Over-Sharing

Public groups are created for specific access grants and rarely cleaned up. The check queries the standard share tables and looks for RowCause = 'Manual' rows where the group membership includes "All Internal Users" — which is effectively an OWD-wide data grant dressed up as a sharing rule.

Checked tables: AccountShare, CaseShare, ContactShare, OpportunityShare. If Health Cloud is installed, HealthCloudGA__EhrCondition__Share is included.

The finding lists the specific groups and objects involved so you can review intent: sometimes broad sharing is deliberate, but it should be a conscious decision, not an artifact of a three-year-old setup task.


Field-Level Security and Scheduled Apex

Field-Level Security: Checks whether sensitive fields (SSN, credit card numbers, and similar patterns by field name) are accessible to profiles beyond System Administrator. FLS misconfigurations are easy to introduce and rarely surface in standard org reviews.

Scheduled Apex: Lists active scheduled jobs and the users they are configured to run as. This becomes relevant the moment that user is deactivated or their permissions change — the job continues to run under a potentially misconfigured or frozen account.

Configurable Scoring

The original scoring model had fixed severity weights. Every org is different — a Health Cloud implementation treats guest user access differently from a Sales Cloud org with no external portals. The --scoring-config flag lets you bring your own weights.

sf audit security --target-org myOrg --scoring-config ./my-scoring.json

The config file has three optional sections. Omit any section to keep the default:

{
  "riskScores": {
    "CRITICAL": 10,
    "HIGH": 7,
    "MEDIUM": 4,
    "LOW": 1,
    "INFO": 0
  },
  "checkWeights": {
    "guest-user-access": 15,
    "hardcoded-credentials": 10,
    "flows-without-sharing": 7,
    "scheduled-apex": 1
  },
  "gradeThresholds": {
    "A": { "minScore": 85, "maxHigh": 0 },
    "B": { "minScore": 70, "maxHigh": 1 },
    "C": { "minScore": 55, "maxHigh": 3 },
    "D": { "minScore": 40, "maxCritical": 0 },
    "F": {}
  }
}

riskScores controls how much each severity level deducts from the starting score of 100.

checkWeights overrides the weight for a specific check, independent of its risk level. A Health Cloud org might increase guest-user-access from the default 10 to 15, or reduce scheduled-apex to near-zero if that is actively monitored elsewhere.

gradeThresholds redefines the grade bands entirely. The existing architecture post noted that any CRITICAL finding results in an immediate Grade F. That behaviour is now configurable via gradeThresholds — the default config preserves it, but you can adjust the thresholds to match your organisation's risk acceptance criteria.

A scoring.sample.json ships with the plugin covering all 23 checks. Copy it, adjust what matters to your org, and pass it with --scoring-config.

Externalized Queries

All SOQL and Tooling API queries now live in two JSON files outside the TypeScript source: config/queries/soql.json and config/queries/tooling.json. Each entry declares its API type, the query or REST path, a description, and an optional fallbackOnError flag:

{
  "activeStandardUsers": {
    "api": "soql",
    "soql": "SELECT Id, Username, ProfileId FROM User WHERE IsActive = true AND UserType = 'Standard'",
    "description": "All active standard users — used by MFA, admin, and inactive checks"
  },
  "profileIpRanges": {
    "api": "soql",
    "soql": "SELECT ProfileId, StartAddress, EndAddress FROM ProfileLoginIpRange",
    "description": "Profile login IP ranges — not queryable in all org configurations",
    "fallbackOnError": true
  }
}

The fallbackOnError flag is worth calling out. Some queries — profileIpRanges being a clear example — are not available in all org configurations. Rather than crashing the audit when the query fails, the registry handles it gracefully and continues. Without this, running the tool in a Developer Edition org or a restricted sandbox would abort the entire run.

The architecture post described the original QueryRegistry as an abstraction that "didn't pull its weight" at the time. That assessment was accurate when the tool ran 22 checks with simple queries. At 23 checks, cross-referencing between checks (Named Credentials vs. Apex endpoints, Health Cloud package detection feeding into three separate checks), and fallback logic, keeping queries inline would mean hunting through 1,500 lines of TypeScript to change a field name. The abstraction has earned its place.


Clickable Findings

High-risk permission findings — ModifyAllData, ViewAllData, AuthorApex, CustomiseApplication — now show each user as a clickable link:

[admin@example.com (System Administrator)](https://myorg.lightning.force.com/005XXXXXXXXXXXX)

The link is constructed from sf_instance on the active connection plus the User record ID. In practice this means going from a finding to the user's Setup page in one click rather than copying a username and searching manually.


Running the updated audit against your org? Start with the usage guide for installation and flag reference. The architecture post covers the cache dependency system and scoring model in depth.

Comments (0)

Loading comments...