Well-Architected anti-patterns
vulkro-sf ships two distinct commands:
vulkro-sf scan: security findings. The default Vulkro surface: CRUD/FLS, injection, XSS, secrets, sharing, posture, identity, third-party, AI / Agentforce. The output gates the AppExchange readiness report.vulkro-sf antipatterns: Salesforce Well-Architected anti-pattern detection. The output is a separate report.
The two commands are intentionally split. This page explains why, documents the AP-001 through AP-014 series, and shows the invocation.
What this command is not
vulkro-sf antipatterns is not security analysis. The findings
it emits do not map to the AppExchange Security Review checklist
sections, do not carry OWASP categories, and do not raise CVSS-style
severity. They are architecture-quality findings about
governor-limit risk, code maintainability, test-class coverage, and
the operational patterns Salesforce architects publish as the
Well-Architected Framework (see
Methodology section 1).
A SOQL query inside a for loop is a Well-Architected anti-pattern
(AP-001): the org will hit the SOQL query governor limit at scale
and the operation will fail at runtime. That is a reliability and
maintainability problem, not a security problem. Mixing it into the
security finding stream would confuse the signal both for engineers
triaging security and for reviewers reading the AppExchange
readiness report.
Why it is split from scan
Three reasons:
- Different signal class. Security findings answer "could this be exploited?". Anti-pattern findings answer "will this break at scale?" or "is this idiomatic Salesforce?". Mixing the two surfaces in one stream makes triage harder.
- Different severity model. Security severity tracks exploitability and blast radius. Anti-pattern severity tracks how reliably the pattern will fail at production volume and how expensive the fix is later. The two are not directly comparable and should not share a Critical / High / Medium / Low scale.
- Different consumer. AppExchange Security Reviewers read the security output; engineering leads and Salesforce architects read the anti-pattern output. The audiences want different reports; they get different commands.
The AP-001 through AP-014 series
Anti-pattern IDs are stable. Each ID is permanent once shipped; a finding's ID does not move between releases. The full series is in the table below; each row links to its detection deep-dive in the Methodology coverage matrix.
| ID | Pillar / Dimension | Pattern |
|---|---|---|
| AP-001 | Trusted / Reliable | SOQL query inside a for / while / do loop. Hits the per-transaction query governor limit (100 SOQL queries in synchronous Apex). Fix by bulkifying: collect IDs first, query once outside the loop. |
| AP-002 | Trusted / Reliable | DML statement inside a loop. Hits the per-transaction DML statement limit (150). Same bulkification fix as AP-001. |
| AP-003 | Adaptable / Future-Proof | Hardcoded Salesforce record ID. A 15- or 18-character ID baked into source. Breaks on org migration; couples the package to a single org. Use Custom Metadata, Custom Settings, or Named Credentials instead. |
| AP-004 | Trusted / Reliable | Apex class with no apparent test class. Cross-file: looks for a sibling-named *Test.cls or a substring reference in any @isTest class. AppExchange requires 75% code coverage. |
| AP-005 | Adaptable / Composable | Multiple triggers on the same SObject. Two or more trigger files target the same on SObject. Order of execution becomes implementation-defined. Use a single trigger plus a handler pattern. |
| AP-006 | Trusted / Reliable | Recursive trigger without a static guard. A trigger that performs DML on Trigger.new or Trigger.old without a static-boolean recursion guard. Re-enters on the same transaction. |
| AP-007 | Trusted / Reliable | Mixed-DML transaction (setup + non-setup sObjects). A single transaction touching a setup object (User, Group, UserRole, Profile, QueueSObject, etc.) and a non-setup object (Account, custom object, etc.) without separating them with System.runAs or async. Raises MIXED_DML_OPERATION at runtime. Excludes @isTest and explicit System.runAs blocks. |
| AP-008 | Easy / Automated | Schema.describe* call inside a loop. Schema.SObjectType.X.fields.getMap() and similar describes are expensive; calling them per iteration in a loop wastes CPU. Cache the describe result outside the loop. |
| AP-009 | Adaptable / Composable | @future method abuse: callout-violation patterns. @future(callout=true) invoked from a context that already issued a callout, or @future chained from another @future. Use Queueable for chained async or Continuation for parallel callouts. |
| AP-010 | Trusted / Reliable | @isTest(SeeAllData=true). Production data visibility in tests. Brittle (depends on org data state), forbidden by AppExchange Security Review, and prevents parallel test execution. Use a TestDataFactory pattern with @testSetup. |
| AP-011 | Trusted / Reliable | Mixed-DML transactional rules. Stricter check than AP-007: flags constructor patterns that build setup-object instances and non-setup-object instances within the same file scope, even when no DML has fired yet, because the pattern is a near-certain mixed-DML at the next persistence step. |
| AP-012 | Adaptable / Composable | Hardcoded URL in callout (use Named Credential). HttpRequest.setEndpoint('http://...') or setEndpoint('https://...') where the argument is a literal URL string rather than a callout:NamedCredentialName/path reference. Named Credentials let the org admin rotate the credential and the endpoint without a deployment. |
| AP-013 | Trusted / Reliable | Non-restrictive SOQL. SELECT ... FROM ... with no WHERE and no LIMIT. Full-object scan; hits the row limit on a real org. |
| AP-014 | Trusted / Reliable | Test class methods without assertions. Test methods that call into production code but do not call any Assert.* or System.assert*. The test runs and contributes to coverage but does not actually verify behavior. AppExchange Security Review reads this as low test quality. |
Invocation
vulkro-sf antipatterns ./force-app
Same input shape as vulkro-sf scan: an SFDX project root, a
retrieved metadata folder, or any directory containing Apex source.
The output is grouped by anti-pattern ID, lists every triggering
file and line, and follows the standard exit-code contract:
0: no anti-patterns triggered.1: at least one anti-pattern fired.2: error.
Output formats
--format accepts the same shapes as vulkro-sf scan:
vulkro-sf antipatterns ./force-app --format text # default
vulkro-sf antipatterns ./force-app --format sarif -o ap.sarif
vulkro-sf antipatterns ./force-app --format json -o ap.json
vulkro-sf antipatterns ./force-app --format html -o ap.html
The SARIF output uses a separate Vulkro tool name
(vulkro-sf-antipatterns) so downstream tools that consume both
streams can split the views cleanly.
CI gating
The recommended pattern is to gate vulkro-sf scan (security) on
PR, and to gate vulkro-sf antipatterns on the nightly run or
on main-branch pushes. Anti-patterns rarely need to block a single
PR; they need to block a release. See
CI/CD integration for the wiring.
Where to go next
- CLI reference:
vulkro-sf antipatterns: the full flag matrix and return-shape contract. - Methodology section 9: the per-anti-pattern detection deep-dive, with pillar mappings and the "what we cover, what is queued" rows.
- Salesforce Well-Architected Framework: the authoritative source for the three pillars (Trusted, Easy, Adaptable) the anti-pattern IDs map to.