agentleFS
grzmolgrzmol/vibe-force/skills/sf-org-security-audit/SKILL.md

sf-org-security-audit

Skill2 starsChanged 2 months agoMITView on GitHub

Standing security audit of a Salesforce org and its source - who holds ModifyAllData, ViewAllData, AuthorApex and ViewEncryptedData, the object and field matrix behind every permission set, profile sprawl, Apex class and @AuraEnabled exposure, org-wide defaults and without-sharing code, guest user and Experience Cloud surface, Named and External Credentials, secrets in source and in git history, and the Setup-only checks (Health Check, session and password policy, login IP ranges, MFA, Setup Audit Trail) that no CLI command covers. Produces a severity-ranked findings report with evidence and a fix per item. Use when onboarding an org, preparing a security review or audit response, or periodically. Not for reviewing a diff - that is the sf-security-reviewer agent - and not for writing secure Apex, which is skill sf-security-model.

The file

We split this skill into the rules it gives an agent

5 rules. About 0 are common, 2 are distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Every command below writes into $AUDIT, so the report can cite raw output rather than memory.

    line 63
  2. Very specific:

    Every @AuraEnabled and @RestResource method is an API an authenticated user can call directly: the client never enforces anything.

    line 170
  3. Distinctive:

    If the org has Sites or Experience Cloud, the guest user is an unauthenticated identity with real permissions.

    line 196
  4. Distinctive:

    No CLI command covers these.

    line 220
  5. Very specific:

    Write it to .vibeforce/reports/security-audit-<ISO>.md.

    line 260

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Salesforce Org Security Audit

When to use

SituationUse this skill
Auditing the standing security posture of an orgYes
Preparing for a customer or internal security reviewYes
Onboarding an org you did not configureYes
Reviewing the security of the diff you just wroteAgent sf-security-reviewer
Writing Apex that enforces CRUD, FLS and sharingSkill sf-security-model
Maintainability rather than exposureSkill sf-technical-debt-audit
Callout authentication designSkill sf-integration-patterns
Analyzer security rules on sourceSkill sf-code-analyzer-quality

This audit is read-only: queries, retrieves and greps. It never changes a permission, a setting or a record, and it is legitimate against production.

Scope and safety

RuleWhy
Read-only commands only: sf data query, sf project retrieve preview, sf org list ..., vf-check staticAn audit that mutates is an incident
Never VF_ALLOW_PROD=1That flag exists to let a mutating check through; nothing here needs it
Retrieve into a scratch directory, never over the working treeA retrieved profile overwriting source is a silent change
Evidence or it did not happenEvery finding carries a query result, a file:line, or a named Setup page
Handle findings like the vulnerability they describeThe report names exposures; treat it as confidential and never paste org ids, usernames or tokens into a public channel

Production posture: vf-check prints a production banner for read-only checks rather than refusing them (scripts/checks/lib/org.mjs). Queries against a production alias are fine here. Running vf-check apex against production is not part of this audit.

Audit layers

LayerThe questionHow to measure
IdentityWho can log in, from where, with what factorSetup: session, password, login IP, MFA; sf org list users
GrantsWho holds administrative permissionsPermissionSetAssignment SOQL, pattern 3
Access matrixWhat each permission set opens upObjectPermissions and FieldPermissions SOQL, pattern 4
Record accessOrg-wide defaults, sharing rules, without sharing codeSource <sharingModel> plus grep, pattern 7
Code exposure@AuraEnabled, @RestResource, webService, Apex class accessSource inventory plus SetupEntityAccess, pattern 6
Public surfaceGuest users, Sites, Experience Cloudnetworks/ metadata plus guest profile, pattern 8
IntegrationNamed and External Credentials, Remote Site Settings, connected appsSource inventory, pattern 9
SecretsLiteral credentials in source or historyGrep plus git history, pattern 9
Change controlWho changed security settings, and whenSetup Audit Trail, pattern 10

Core patterns

1. Fix the scope and the evidence trail

sf org display --target-org vf-prod
sf org list users --target-org vf-prod
mkdir -p .vibeforce/reports/audit && export AUDIT=.vibeforce/reports/audit

Record the org id, the edition, the alias, whether the source tree is the whole org, and the date. Every command below writes into $AUDIT, so the report can cite raw output rather than memory.

2. Retrieve the security-relevant metadata

<!-- manifest/security-audit.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
    <types><members>Security</members><name>Settings</name></types>
    <types><members>*</members><name>PermissionSet</name></types>
    <types><members>*</members><name>PermissionSetGroup</name></types>
    <types><members>*</members><name>Profile</name></types>
    <types><members>*</members><name>SharingRules</name></types>
    <types><members>*</members><name>NamedCredential</name></types>
    <types><members>*</members><name>ExternalCredential</name></types>
    <types><members>*</members><name>RemoteSiteSetting</name></types>
    <version>67.0</version>
</Package>
sf project retrieve start --manifest manifest/security-audit.xml \
  --target-org vf-prod --output-dir "$AUDIT/metadata"

Retrieving into --output-dir keeps the working tree untouched. The directory layout under it depends on the retrieve, so locate the retrieved types rather than assuming a path:

find "$AUDIT/metadata" -type d \( -name profiles -o -name permissionsets -o -name permissionsetgroups \
  -o -name namedCredentials -o -name externalCredentials -o -name remoteSiteSettings -o -name sharingRules \)

An ls against a guessed path that does not exist prints an error and counts as zero - on a security audit that reads as a clean result. Always count from find output.

Profiles retrieve as a sparse representation - a permission missing from a retrieved profile is not proof it is not granted, so confirm grants with the queries in patterns 3 and 4 rather than by reading profile XML.

3. Who holds the dangerous permissions

sf data query --target-org vf-prod --json \
  --query "SELECT Assignee.Username, Assignee.IsActive, PermissionSet.Name FROM PermissionSetAssignment WHERE PermissionSet.PermissionsModifyAllData = true" > "$AUDIT/modifyalldata.json"

sf data query --target-org vf-prod --json \
  --query "SELECT Assignee.Username, PermissionSet.Name FROM PermissionSetAssignment WHERE PermissionSet.PermissionsViewAllData = true" > "$AUDIT/viewalldata.json"

Repeat for PermissionsAuthorApex, PermissionsViewEncryptedData, PermissionsManageUsers and PermissionsCustomizeApplication [unverified: only PermissionsModifyAllData, PermissionsViewAllData, PermissionsViewAllRecords and PermissionsModifyAllRecords are confirmed field names in this repository - run sf sobject describe --sobject PermissionSet --target-org <alias> before relying on the rest]. Findings, in descending severity:

FindingSeverity
An integration or service account holds ModifyAllDatahigh
A named person holds ModifyAllData outside the admin grouphigh
An inactive user still holds an administrative permissionmedium
ViewAllData granted for a reporting need a report type would satisfymedium
AuthorApex outside the release processmedium

A grant that appears through a profile's own permission set still counts as a grant, which is why profile sprawl (pattern 5) is a security topic and not only a tidiness one. [unverified: the IsOwnedByProfilefield used below to separate profile-owned permission sets from standalone ones was not confirmed in this session - describe the object first, or drop the filter and group byParent.Name.]

4. The access matrix per permission set

sf data query --target-org vf-prod --json \
  --query "SELECT Parent.Name, SobjectType, PermissionsRead, PermissionsCreate, PermissionsEdit, PermissionsDelete, PermissionsViewAllRecords, PermissionsModifyAllRecords FROM ObjectPermissions WHERE Parent.IsOwnedByProfile = false" > "$AUDIT/objectperms.json"

sf data query --target-org vf-prod --json \
  --query "SELECT Parent.Name, Field, PermissionsRead, PermissionsEdit FROM FieldPermissions WHERE SobjectType = 'Account'" > "$AUDIT/fieldperms-account.json"

Audit the sensitive objects first: anything holding personal data, financial data, or credentials. PermissionsViewAllRecords and PermissionsModifyAllRecords bypass sharing for that object - each one needs a named reason, and an object-level bypass granted to a broad permission set is a high finding.

5. Profile sprawl

find "$AUDIT/metadata" -path "*/profiles/*.profile-meta.xml"        | wc -l
find "$AUDIT/metadata" -path "*/permissionsets/*.permissionset-meta.xml" | wc -l

Posture, in order of preference: permission set groups for personas, permission sets for features, profiles reduced to login and default settings. A large profile count with few permission sets means access cannot be reviewed - the finding is "grants are not reviewable", and its severity depends on what those profiles open up (pattern 4), not on the count.

6. Code exposure surface

grep -rn "@AuraEnabled" force-app --include="*.cls" | wc -l
grep -rn "@RestResource\|global class\|webService" force-app --include="*.cls"
grep -rln "without sharing" force-app --include="*.cls"

sf data query --target-org vf-prod --json \
  --query "SELECT Parent.Name, SetupEntityId FROM SetupEntityAccess WHERE SetupEntityType = 'ApexClass'" > "$AUDIT/apexclassaccess.json"

Every @AuraEnabled and @RestResource method is an API an authenticated user can call directly: the client never enforces anything. Check each one for user-mode data access and a sharing keyword (skill sf-security-model). A generic endpoint - "run this SOQL", "update this record with this map" - is a high finding regardless of who holds the class access.

7. Record access: org-wide defaults and elevation

grep -rh "<sharingModel>" force-app/main/default/objects/*/*.object-meta.xml | sort | uniq -c
grep -rn "without sharing" force-app --include="*.cls"
grep -rn "__Share\b" force-app --include="*.cls"

An object whose sharing model is ReadWrite needs a reason; Private plus sharing rules is the defensible default for anything sensitive. Every without sharing class and every Apex managed sharing insert is an elevation path: it must be narrow, documented, and covered by a negative test (skill sf-security-model, references sharing-and-record-access.md).

8. Public surface

ls force-app/main/default/networks/ 2>/dev/null
ls force-app/main/default/sites/ 2>/dev/null
grep -rn "Site\.\|Network\.\|UserInfo.getUserType" force-app --include="*.cls"

If the org has Sites or Experience Cloud, the guest user is an unauthenticated identity with real permissions. Audit it as its own user: what objects it reads, which Apex classes it can run, which Flows it can start, and whether any without sharing code runs on its behalf. Guest access to person data is a high finding on sight, and personal-information visibility settings on the User object are not enforced by user mode or stripInaccessible (skill sf-security-model, reference crud-fls-enforcement.md).

9. Integrations and secrets

find "$AUDIT/metadata" -path "*/namedCredentials/*"    -type f
find "$AUDIT/metadata" -path "*/externalCredentials/*" -type f
find "$AUDIT/metadata" -path "*/remoteSiteSettings/*"  -type f

grep -rniE "password|secret|token|api[_-]?key|authorization" force-app --include="*.cls" --include="*.js" --include="*.xml"
git log -p --all -S"password" -- force-app | head -100

A Remote Site Setting with no matching Named Credential usually means a callout is authenticating by hand, with the credential somewhere in source. A secret found in git history is still a secret: the finding is "rotate the credential", not "remove the line".

10. The Setup-only layer

No CLI command covers these; a person opens Setup and records the answer. They belong in the report with the same severity scale as everything else.

CheckSetup locationLooking for
Health Check score and risksSecurity → Health CheckScore, and every High-Risk setting against the baseline
Session settingsSecurity → Session SettingsTimeout, lock to IP, clickjack protection, HttpOnly
Password policySecurity → Password PoliciesExpiry, history, complexity, lockout
MFAIdentity → and the MFA assistantEvery interactive user covered
Login IP ranges and login hoursProfilesAdmin profiles restricted
Connected apps and OAuth scopesApps → Connected Appsfull and refresh_token scopes, who approved them
API accessProfiles and permission setsAPI Enabled on people who do not integrate
Certificate and key managementSecurity → Certificate and Key ManagementExpiry dates
Setup Audit TrailSecurity → View Setup Audit TrailSecurity changes in the last 6 months, and by whom
Shield Platform Encryption, Event MonitoringDepends on licensingWhether they are available and used

[unverified: whether the Health Check score, session settings and Setup Audit Trail are readable through the API in this org's edition was not confirmed in this session. Treat this layer as manual until you verify it with sf sobject describe against the relevant object.]

11. The report

## Security audit - <org alias> - <ISO date>
Scope: <what was audited>   Not audited: <what was out of reach>

| severity | layer | finding | evidence | fix | owner |
| --- | --- | --- | --- | --- | --- |

severities: high (data exposure or privilege escalation available today)
            medium (grant wider than needed, or a control with a gap)
            low (hardening opportunity, no current exposure)

## Setup-only layer
| check | result | finding |

## Accepted risks
<item> - <accepted by whom> - <review date>

Write it to .vibeforce/reports/security-audit-<ISO>.md. A re-run diffed against the previous report is what turns an audit into a control.

Anti-patterns

Anti-patternConsequenceFix
Reading retrieved profile XML and concluding a permission is absentProfiles retrieve sparsely; the grant may still existConfirm with ObjectPermissions and PermissionSetAssignment queries
Auditing source onlyOrg-only grants and Setup settings stay invisibleRetrieve, query, and record the Setup-only layer
Reporting "too many profiles" as the findingCount is not exposureName what the profiles grant, with evidence
Fixing a finding during the auditThe audit becomes an unreviewed changeReport; remediate through the normal gated path
Running the audit as an integration userYou see only what that user seesRun as an admin, and say which user produced the evidence
Pasting query output with usernames or org ids into a ticketThe report becomes the exposureRedact identities; reference row counts and ids held separately
Treating a clean Code Analyzer run as a security auditStatic rules cover code, not grantsBoth; they measure different layers
Leaving [unverified] items out of the reportUnknowns read as passesList them under "Not audited"

Verification

# static security rules over the whole tree, not the diff
node "${CLAUDE_PLUGIN_ROOT}/scripts/checks/vf-check.mjs" analyzer --json
node "${CLAUDE_PLUGIN_ROOT}/scripts/checks/vf-check.mjs" static --json

# grants, read-only, production-safe
sf data query --target-org vf-prod --query "SELECT COUNT(Id) FROM PermissionSetAssignment WHERE PermissionSet.PermissionsModifyAllData = true"

# what a retrieve would bring back, without touching the working tree
sf project retrieve preview --target-org vf-prod

Exit codes: 0 pass, 1 gate failed, 2 misconfiguration or missing tool, 3 org or network error. A finding in this audit is not a vf-check failure - the gates run on the diff; the audit runs on the org. Both belong in the report.

References

  • references/org-security-checklist.md - the full checklist: per-layer checks, the query or Setup path, the expected answer, and the severity when it differs.
  • Related skills: sf-security-model (enforcement in code, permission architecture, sharing), sf-technical-debt-audit (the maintainability half of the same sweep), sf-integration-patterns (Named and External Credentials), sf-code-analyzer-quality (security rules and custom regex rules), sf-data-management (data exports and PII handling), sf-debugging-logs (Event Monitoring).
  • Agent sf-security-reviewer for the per-diff review that this audit does not replace.
  • Official: Apex Security and Sharing, Enforce User Mode, stripInaccessible, Named Credentials.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.