Using AI to Prepare for Salesforce Security Review
The first managed package I took through AppExchange security review came back with 412 findings, and I opened the report at 5pm on a Friday. Most of them were the same three CRUD/FLS patterns repeated across forty-odd classes, but the scanner doesn't group them for you — it lists every line, every field, every query, flat. I spent the next fortnight doing what amounts to find-and-replace with a security posture, by hand, one class at a time.
I don't do it that way anymore. The remediation hasn't changed — the scanner still wants the same patterns it always did. What's changed is that I now run the whole triage-and-fix loop with AI sitting in the middle of it, and the fortnight is closer to two days. This is the workflow I actually use, including the parts where I stopped trusting the AI and went back to doing it myself.
The Scanner Report Is a Triage Problem, Not a Coding Problem
The security review is two automated scans and a human. The first scan is the Salesforce Code Analyzer, which bundles PMD and a set of security-focused rules. The second is a Checkmarx source scan that partners can request through the Partner Security Portal. Both produce long, flat lists of findings. The human reviewer reads your questionnaire and makes judgement calls on top of what the scanners found.
Here's the thing most people get wrong on their first submission: they treat the report as a coding problem, opening each file and fixing findings top to bottom. It isn't a coding problem. It's a triage problem. A 400-finding report is usually 6 or 7 distinct root causes repeated across the codebase, plus a handful of genuine one-offs, plus false positives the scanner flags because it pattern-matches instead of tracing every path.
The work that eats your week is the categorising, not the fixing. And categorising a wall of near-identical findings by root cause is exactly the kind of tedious, structured reading that AI is good at and I am not.
Running the Scan Before Salesforce Does
The first rule is to never let Salesforce be the one to run the scanner on your code. Run it yourself, locally, and see what they'll see. The Code Analyzer is a CLI plugin, so it drops straight into your existing workflow:
# Salesforce Code Analyzer v5 — the security rule set is the one that
# maps most closely to what the AppExchange review flags
sf code-analyzer run \
--rule-selector security \
--workspace force-app \
--output-file scan.json \
--view detail
That gives you a structured JSON report. The next step is where AI comes in — I hand the report to Claude Code with the whole codebase already in its context, and ask it to think in root causes rather than line numbers:
claude "Read scan.json. Group the findings by rule AND by underlying root-cause
pattern, not by file. For each group tell me: how many findings it covers,
whether it's a real issue or a likely scanner false positive, and the single
remediation that would clear the whole group. Do not change any code yet."
What comes back is the report I wish the scanner produced: "340 findings, all the same missing FLS check on SOQL results returned to Aura — one fix pattern clears all of them" instead of 340 separate lines. That single reframing is most of the value. It turns an overwhelming list into a to-do list with maybe a dozen items on it, ordered by how many findings each one clears.
If you've read how I use Claude Code to write Apex faster, it's the same shape — the AI's advantage isn't better Apex, it's that it reads the entire codebase in one pass and never gets bored on finding 290 of 412.
Batch-Fixing CRUD and FLS Without Losing a Week
Once the findings are grouped, the largest group is almost always CRUD/FLS on query results. This is the single most common reason packages fail first review, and I've written about the enforcement patterns that actually pass in detail. The remediation is mechanical, which is precisely why it's worth handing to AI.
A typical flagged method looks like this — a query result returned straight to a Lightning component with no field-level check:
public with sharing class QuoteController {
@AuraEnabled
public static List<Quote> getQuotes(Id opportunityId) {
return [SELECT Id, Name, GrandTotal, Status
FROM Quote
WHERE OpportunityId = :opportunityId];
}
}
I don't ask the AI to fix this one method. I ask it to fix every method that matches the pattern, using the enforcement style already in the package, and to show me the diff before touching anything:
"Every finding in the 'FLS on Aura-returned SObjects' group: rewrite the query to use
WITH SECURITY_ENFORCEDand pass the result throughSecurity.stripInaccessible(AccessType.READABLE, ...)before returning. Match the existingSecurityUtilshelper if the class already imports it. Show me the full diff across all affected files first. Change nothing else."
The fixed method it produces is unremarkable, which is the point:
public with sharing class QuoteController {
@AuraEnabled
public static List<Quote> getQuotes(Id opportunityId) {
List<Quote> quotes = [
SELECT Id, Name, GrandTotal, Status
FROM Quote
WHERE OpportunityId = :opportunityId
WITH SECURITY_ENFORCED
];
SObjectAccessDecision decision =
Security.stripInaccessible(AccessType.READABLE, quotes);
return decision.getRecords();
}
}
The AI gets this right about 80% of the time across a batch. The other 20% is where it doubles up a check that a helper already applies, or strips a field the UI genuinely needs and quietly breaks a component. So I read every diff. The trade-off here is blunt: the AI does the typing across 40 files, but I still owe an hour of disciplined diff review, and that hour is not optional. The moment I treat its output as trusted rather than as a draft, I ship a regression.
The Questionnaire Is Where AI Earns Its Keep
The scanners are deterministic. The human review is not, and the security questionnaire is how you talk to the human. For every finding you've suppressed, every without sharing class, every pattern that looks unsafe but isn't, you write a paragraph explaining the use case, why the safer alternative doesn't fit, and what risk you've weighed.
This is genuinely tedious writing, and it's the task where AI has saved me the most time — not because it writes the final justification, but because it drafts a structured first pass from the code. I point it at a flagged class and ask for the skeleton:
"This class is
without sharingand the scanner flagged it. Read the class, explain in plain English why it runs without sharing, what data it touches, and what would break if it ranwith sharing. Draft it as a security questionnaire justification. Flag anything you're unsure about rather than guessing."
What comes back is a competent draft that gets the mechanics right and the risk framing generic. I then rewrite it, because a reviewer who reads 50 packages a week can smell boilerplate, and boilerplate is what gets you a second review cycle. The AI writes the scaffolding; I write the reasoning and own every word of it. That division of labour is the one I'd defend hardest: AI drafts the questionnaire, a human signs it.
412 Findings, One Afternoon, and the Review We Still Failed
The clean version of this story is that AI turned my fortnight into an afternoon and we sailed through. That's half true.
On a more recent fintech ISV package, the initial Checkmarx scan came back with 412 findings — near enough to my first-ever number that it stuck with me. Running the triage prompt over the report, the AI bucketed roughly 340 of them into a single FLS pattern and another 50 into missing with sharing declarations. Rohit, the developer on the team, ran the batch fixes against the two big groups in an afternoon, I reviewed the diffs the next morning, and the scanner-visible count dropped to single digits. That part worked exactly as advertised.
Then we failed the human review. There was one without sharing class doing cross-account aggregation for a reporting feature, and the AI's questionnaire draft had confidently described it as a legitimate, low-risk pattern. It read well. I edited it lightly and submitted it. The reviewer disagreed, asked us to prove the aggregation couldn't leak records across tenants, and bounced the submission. We ended up refactoring the class to run the aggregation through an explicitly access-checked path anyway — which is what a more careful human read of that class would have told me to do in the first place. We passed on the second cycle, but the AI didn't save us that round. It arguably cost us one, because a plausible-sounding draft made me stop thinking about a class that deserved more thought.
Where the AI Stops Being Useful
The dangerous failure mode isn't the AI missing a fix. It's the AI labelling a real vulnerability as a false positive, in a confident tone, in a questionnaire paragraph you're about to sign. The scanner over-flags; the AI, asked to explain away flags, will sometimes over-reassure. Those two biases point the same direction, and that direction is "ship the vulnerability."
So there's a fixed line I don't let AI cross. It doesn't get the final call on whether a finding is a false positive — it can propose, I decide. It doesn't get to judge whether a without sharing justification is adequate, because that's a call about the reviewer's risk tolerance, and the AI has never met the reviewer. And it doesn't get to write SOQL-injection remediation unsupervised, because whether a dynamic query is safe depends on your actual data and binding, which the model can't see from the source alone.
For the security-critical judgement calls, I read the code myself, the old way, and I treat the AI as a fast junior who's read everything and understood most of it. If you're taking a package through review and want a second pair of eyes on the questionnaire and the patterns before Salesforce sees them, that's the kind of review I do — usually the cheapest hour in the whole project, because it catches the class that would have cost you a submission cycle. The same principle runs through building a 2GP package from scratch: security review is the real project, and the automation only helps with the mechanical half.
The scanner report used to be the start of a bad fortnight. Now it's an afternoon of triage and a questionnaire that mostly writes itself — right up until the paragraph that actually matters, which is still mine to write and mine to get wrong.
Liked this?
New essays in your inbox when I publish. No cadence, no spam.
You might also like
How AI Changes the Salesforce Consulting Model
The economic shifts AI is bringing to Salesforce consulting — what's getting cheaper, what's getting more valuable, and what the agency model looks like now.
Read →How I Use Claude Code to Write Apex 5x Faster
The actual workflow I run for AI-assisted Apex development — what AI handles well, what it gets wrong, and the prompts that survive code review.
Read →AI-Assisted Salesforce Data Migration: A Real Walkthrough
How I actually use AI across a Salesforce data migration — profiling, field mapping, and the transform Apex — plus the near-miss that set my one hard rule.
Read →