Skip to main content

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:

  1. 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.
  2. 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.
  3. 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.

IDPillar / DimensionPattern
AP-001Trusted / ReliableSOQL 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-002Trusted / ReliableDML statement inside a loop. Hits the per-transaction DML statement limit (150). Same bulkification fix as AP-001.
AP-003Adaptable / Future-ProofHardcoded 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-004Trusted / ReliableApex 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-005Adaptable / ComposableMultiple 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-006Trusted / ReliableRecursive 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-007Trusted / ReliableMixed-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-008Easy / AutomatedSchema.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-009Adaptable / 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-010Trusted / 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-011Trusted / ReliableMixed-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-012Adaptable / ComposableHardcoded 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-013Trusted / ReliableNon-restrictive SOQL. SELECT ... FROM ... with no WHERE and no LIMIT. Full-object scan; hits the row limit on a real org.
AP-014Trusted / ReliableTest 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