Core Concepts
Security & code quality#
Every pull request into this package is scanned with Salesforce's own Code Analyzer. This page publishes what the latest scan found — including the findings that are still open — because a number you can check is worth more than an assurance you can't.
The short version#
| Critical findings | 0 — and 0 on every scan to date |
| Known-vulnerable JavaScript libraries | 0 — RetireJS returns nothing across the shipped package |
| Card data stored in Salesforce | None. Stripe.js tokenizes in the browser; only a payment-method ID reaches Apex |
| Public inbound endpoints | None. Salesforce asks Stripe for payment status — nothing is pushed into your org |
| API secrets in package fields | None. Stripe keys live only in a Named Credential your admin creates |
| Apex test coverage (packaged version) | 95%, measured by Salesforce, against a 75% requirement |
What removes risk before a scanner sees it#
A static analyzer can only find problems in code that exists. Four architectural decisions mean large categories of them are never written in the first place.
No cardholder data ever reaches your org. The donation form loads Stripe's Payment Element, and the card is tokenized in the donor's browser. What Apex receives is an opaque payment-method identifier. There is no field on any object in this package that could hold a card number, and nothing to leak if a record is over-shared. See Donations & payments.
No inbound webhook, so no public endpoint. Most payment integrations register a URL that the processor calls back into. This one inverts that: after a donor pays, a scheduled job in Salesforce asks Stripe what happened. There is no exposed endpoint to secure, no webhook signing secret to store or rotate, and no unauthenticated write path into your data.
Secrets are never packaged. The package ships no API key, in a custom field or anywhere else. Your admin creates a Named Credential during setup and Salesforce holds the key; the package references the credential by name. See Connecting Stripe.
The guest user can only insert. The public donation and event pages run as the Experience Cloud guest user, which in this package is granted the narrowest set of permissions the flow demonstrably needs. A guest submission is written as a staging row and picked up by a background job running in an authenticated context — a guest cannot create a Contact or an Opportunity, and cannot read one back.
The latest scan#
Scanned 28 September 2026 · Code Analyzer 0.53.0 · rule selector Recommended
Engines: PMD, ESLint (including the LWC and SLDS rule sets), CPD, RetireJS, and Code Analyzer's
regex engine. Scope is base/ — the packaged source, and only that. The gate measures what a
subscriber installs, so a red build always means the product has a problem, never that a local
build script does.
| Severity | Count |
|---|---|
| Critical | 0 |
| High | 22 |
| Moderate | 923 |
| Low | 129 |
| Info | 126 |
1,200 total, down from 22,064 on 2 September. Almost none of that drop came from switching rules off. Most of it was fixed in the code:
- Every public class, method and property now has an ApexDoc block, vendored FFLib and the
test classes included (2,411 findings on 28 September, now 11: missing
@param/@returntags on private helpers in two test classes). - Field declarations come before methods in every class (0 remaining).
- The vendored FFLib source was normalised to the package's own whitespace, brace and
annotation-casing style (
NoMixedIndentation,NoTrailingWhitespaceandAnnotationsNamingConventionsare all at 0). That reformatting changed layout only, and the full Apex suite passes on the result. - Unused local variables are gone (110 on 28 September, now 0). Where the discarded call mattered (a matcher registration, a call expected to throw), the call was kept and only the variable dropped.
Two rules are disabled in code-analyzer.yml, and each has its reason written next to it. Neither
is a security rule, and neither runs under the AppExchange + Recommended:Security selectors
the AppExchange reviewer uses:
| Rule | Findings when disabled | Why it does not apply here |
|---|---|---|
slds/no-hardcoded-values-slds2 |
12,606 | Asks for every literal CSS value to become an SLDS styling hook. This package styles with its own --pf-* tokens, where the literal is the fallback half of the var(--token, value) pattern: a campaign's own branding wins when it sets a token, and the intended design renders when it doesn't. The rule cannot be satisfied without giving up theming, and its auto-fix would break every campaign that overrides a token. |
ApexUnitTestClassShouldHaveRunAs |
3,064 | Reports every test method that doesn't wrap itself in System.runAs. That's one finding per method, and none of them is in shipped code. Tests whose result depends on who is running them (CRUD/FLS, sharing, guest access) already run as a constructed user. |
What remains is mostly design-size advice:
| Rule | Count |
|---|---|
CyclomaticComplexity |
157 |
ExcessiveParameterList |
170 |
CognitiveComplexity |
103 |
DetectCopyPasteForApex |
105 |
AvoidBooleanMethodParameters |
98 |
FieldNamingConventions |
77 |
None of those six is a defect. The last two ask for renamed fields and changed method signatures on classes a released version already ships, so they are left as they are rather than changed for style. What matters is the severity split above, and the findings below.
Three rules are measured against this project's conventions, not PMD's defaults#
code-analyzer.yml re-registers three PMD rules through a custom ruleset
(scripts/lint/pmd/pledgivo-conventions.xml) so that they test what they are meant to test:
MethodNamingConventionsrejected thetest_thing_doesThingnaming used throughout the test suite, which is a deliberate convention, not an accident — 2,601 findings, none of them actionable. Retuned to this project'stestPattern, it reports none.ApexUnitTestClassShouldHaveAssertsonly recognisesSystem.assert*andAssert.*. Most service, domain and selector tests here assert through fflib-apex-mocks, wheremocks.verify(uow, 1).commitWork()is the assertion — it throws when the interaction did not happen. PMD counted 200 mock-verified tests as having none. Widened to accept mock verification and this repo's named assertion helpers, it reports 15.EmptyStatementBlockreported 123 empty bodies that the language or the architecture requires: overrides of a deliberately abstract hook, interface methods a class does not need, virtual hooks, and private no-arg constructors. Retuned to skip those shapes, it still reports every emptyif, loop or plain method, and finds 4: the explicit no-arg constructors of two vendored FFLib error classes.
Both rules are still enforcing: a method named badly and a test that neither asserts nor verifies are both still reported. That was checked against a deliberately bad fixture rather than assumed, because a mis-registered custom rule and a correctly-registered one look identical on a clean tree — both produce zero findings. The 15 that remain are all in vendored FFLib's own tests.
Every security-tagged finding, and what it is#
The analyzer tags 24 findings across the whole package as security-relevant (scan of 2026-09-28,
AppExchange + Recommended:Security rule selectors — the same command the AppExchange reviewer
runs). All of them are listed here with a verdict; each one is also recorded in the project's
triage record or annotated in the code itself.
| Severity | Rule | Where | Verdict |
|---|---|---|---|
| High | LoadJavaScriptHtmlScript |
stripePayment.page, stripePaymentReturn.page |
The one external script the package loads is Stripe.js, which Stripe requires to be loaded from js.stripe.com and never bundled or pinned, for PCI reasons. It runs in a Visualforce iframe that holds no Salesforce session. |
| Moderate | ProtectSensitiveData (18) |
Payment_Account__c, Donation_Staging__c, Guest_Request__c, Portal_Access_Token__c, Recurring_Donation__c, Settings__c and three Designation__c fields |
A substring match on the field name (Key, Token, Hash, Sign…, Auth). Three hold a Named Credential's name (never a key), five hold hashes or single-use capability tokens that are worthless outside the flow that issued them, and the other ten are Designation, Re_Auth…, Email_Signature, Soft_Credit_Automation… — no secret in any of them. |
| Moderate | ApexDangerousMethods (3) |
DonationDesignations, RecurringDonations, Contacts |
Disables the FFLib trigger-layer CRUD re-check on a rollup that reads and writes only its own totals. The DML itself still runs under the platform's access rules. |
| Moderate | ApexSharingViolations |
fflib_SObjectUnitOfWork |
Vendored FFLib. The access level is supplied by whoever constructs the Unit of Work, so the decision is made one level up, where the analyzer can't follow it. |
Three findings that used to appear in this table are gone rather than justified: the ticket page's
innerHTML write (the QR code is now a template-bound <svg> path, so there is no DOM sink at
all), and eight credential-shaped identifier names that were renamed to say what they actually
hold (a Named Credential endpoint, a setup-checklist label, an email closing).
The other 20 High findings are none of them security-tagged: every one of them is inside vendored FFLib, which this package carries as source so that installing it does not force you to install a library first.
Every one of them is written up individually. The package publishes a Code Analyzer exceptions report listing each finding it does not fix, what the rule flags, why it is not a defect here, and how to check that for yourself against the code. That page is generated from a live scan rather than written by hand, and the build fails if a new finding appears without a justification or if a justification outlives the finding it explains — so it cannot quietly go out of date.
The submission documents#
The two write-ups uploaded with the review are published here as pages, rendered from the same HTML the PDFs are printed from, so neither can drift from what Salesforce was sent:
- Solution architecture: what the package is made of, how a donation moves through it, where data lives, and what is reachable from outside.
- Sample API callouts: every outbound request to Stripe with sample bodies, and the one inbound REST endpoint the package exposes.
The false-positive documents#
Salesforce's review process expects a written response to every finding a partner does not fix, from both scanners it runs. Both responses are published here, and this is their only copy — the same files are what the submission uploads, so a reviewer reading the site and a reviewer reading the submission see the same document.
| Scanner | What it answers | Document |
|---|---|---|
| Force.com Source Code Scanner (Checkmarx), run through the Partner Security Portal on 2026-10-07 | All 273 reported security paths, grouped by rule and resolved to the 91 code locations they point at, and a disposition for each of the 100 code-quality paths | PDF · XLSX |
Salesforce Code Analyzer, AppExchange + Recommended:Security (the command in The latest scan) |
Every finding in the table above, one response each, joined from the same triage record CI enforces. Each response states the scanner's claim in its own words, why that claim is false at that site, the supporting detail, the evidence to check against the code, and the sources it relies on | PDF · XLSX |
Both PDFs follow the False Positive Documentation template Salesforce's security team gives partners: the application, package and version, partner and filing date once, then the template's four questions — vulnerability name, detected by, detailed explanation, evidence — repeated once per finding (one per flagged code location for Checkmarx, one per reported finding for Code Analyzer), with the box the Salesforce reviewer fills in left blank under each one. The explanation has a fixed shape: the scanner's claim (the rule's own description and the statement it points at), one paragraph stating why that claim is false for this statement, the supporting detail, and the Salesforce or vendor documentation each reason rests on; the evidence gives the flagged code (for Checkmarx, the dataflow the scanner reported as well) and what was checked. An appendix records the scan itself and indexes every raw finding to its entry; the Checkmarx one also gives the code-quality paths their dispositions. Each spreadsheet holds the same responses as rows, for anyone who prefers to filter.
They are built, not written: a script in the repository turns the scan output and the triage record into the PDF and spreadsheet, so when a finding is fixed or a verdict changes the document is regenerated from source rather than edited by hand.
What this page does not claim
Code Analyzer is a static analyzer, not an audit, and a clean scan is not a security certification. This package has not completed Salesforce's AppExchange security review; when it does, that will be stated here as a separate fact. What the numbers above establish is that the code is scanned continuously, that the findings are published rather than filtered, and that every security-tagged one has a written verdict.
How it stays this way#
The scan is a required check on every pull request into the main branch, and runs again weekly so that a rule added in a new analyzer release surfaces against unchanged code.
- A Critical finding fails the build. The budget is zero, and the package has never had one, so any Critical is a genuine regression rather than an accepted baseline.
- High findings are capped at the current count. A pull request that adds one fails. Raising the cap is a deliberate, reviewed act that has to arrive with a written verdict for the new finding — it can't be nudged up to turn a build green.
- The analyzer version is pinned, so a red build always means new code, never a new rule quietly appearing.
- Auto-fixes are never applied wholesale — the SLDS rule above is the reason.
Alongside it, a separate workflow audits the package's shipped JavaScript dependencies weekly and on every pull request, and fails on any advisory at any severity.
Test coverage#
145 Apex test classes and over 2,300 test methods ship inside the package. Salesforce recomputes coverage itself when a package version is built and refuses to release one below 75%; the current version measured 95%.
Coverage is also verified across account models a development org can't reproduce. Before a version ships, the same packaged build is installed into four separate orgs — standard Contact/Account, Person Accounts, NPSP, and Person Accounts with NPSP — and the full test suite, a set of probes that push real records through the real packaged triggers, and the browser-level donation flow are all run in each. Results are diffed against the standard-account org, so a difference that only appears under Person Accounts or NPSP is visible instead of being buried in noise common to all four.