“Shift left” is a well-worn phrase in DevSecOps, but in the Salesforce world it’s still mostly aspirational. Most orgs still do security review as a manual gate before production - one person, one checklist, once per release. That doesn’t scale.
Here’s how to bake security into the pipeline so it runs automatically on every commit.
The Cost of Late Security Findings
A security issue found in a developer’s IDE costs minutes to fix. Found in code review: an hour. Found in UAT: a day. Found in production: a week, a breach notification, and a board conversation.
The Salesforce ecosystem has good tooling for early detection - it’s just not widely used yet.
Static Analysis with PMD
PMD with the Apex ruleset is the baseline. It catches the obvious stuff: SOQL in loops, hardcoded IDs, empty catch blocks, missing with sharing declarations.
Add it to your pipeline as a required check:
- name: Run PMD
run: |
pmd check \
--dir force-app \
--rulesets category/apex/security.xml,category/apex/performance.xml \
--format sarif \
--report-file pmd-results.sarif \
--minimum-priority 2
Set --minimum-priority 2 to block on critical and high findings only. You’ll tune this down over time as the codebase improves.
Salesforce Code Analyzer
Salesforce’s own Code Analyzer (formerly Scanner) wraps PMD, ESLint, and RetireJS into a single CLI. It catches Apex security issues, LWC XSS vectors, and known-vulnerable JavaScript libraries:
sf scanner run \
--target "force-app/**/*.cls,force-app/**/*.js" \
--pmdconfig config/pmd-ruleset.xml \
--eslintconfig config/.eslintrc.json \
--severity-threshold 2
The --severity-threshold 2 flag makes the command exit non-zero on high and critical findings, which fails the CI job.
SOQL Injection Detection
PMD catches some SOQL injection patterns, but it misses dynamic queries built across multiple methods. For thorough detection, add a custom rule or use a dedicated scanner:
The key patterns to hunt for:
Database.query()where the string argument includes any variableString.format()used to build SOQL strings- User-controlled input (from
ApexPages.currentPage().getParameters(), REST request bodies, or LWC@wireadapters) flowing into any query
Permission Set and Profile Drift Detection
Security misconfigurations in metadata are just as dangerous as code vulnerabilities. Add a check that fails if any of these appear in a PR’s changed metadata:
- New
Modify All DataorView All Datapermission assignments - New connected app with
full_accessOAuth scope - Any profile change that elevates API access
- Removal of field-level security restrictions on PII fields
A simple grep over the metadata diff catches most of this:
git diff HEAD~1 --name-only | xargs grep -l "modifyAllData>true\|viewAllData>true" && \
echo "Elevated permissions detected - manual review required" && exit 1
Secrets Scanning
Developers commit secrets to Salesforce repos more than you’d think. Connection strings in test classes, hardcoded API keys in custom metadata, auth tokens in anonymous Apex files committed by mistake.
Run a secrets scanner on every push:
- name: Secrets scan
uses: aquasecurity/trivy-action@v0.35.0
with:
scan-type: fs
scanners: secret
exit-code: '1'
Making It Stick
The technical implementation is the easy part. The hard part is culture. Security checks that block developers get worked around. The key is:
- Fast feedback - the scan must complete in under 3 minutes or developers ignore it
- Clear explanations - every failing check must link to a doc explaining why and how to fix it
- False positive process - a clear, documented way to suppress a false positive with a required justification comment
- Gradual ratcheting - start with only critical findings blocking the pipeline, add high findings next quarter, then medium
Security that ships with the pipeline is security that actually runs. Security that lives in a checklist someone consults before a release is security that gets skipped when you’re under deadline pressure.