Skip to content

How We Built a Native sf Plugin for Salesforce Security

We had a working Python script. Here is why we rewrote it as a native sf plugin, and the design decisions that made 22 parallel security checks practical.

TL;DR

  • We rewrote a working Python security script as a native sf plugin so the team that already runs sf can install and run it with no extra toolchain.
  • The plugin is built on a six-layer architecture with a strict downward dependency rule, a dependency-aware cache system, and a configurable scoring model in config/scoring.json.
  • Any CRITICAL finding immediately forces a Grade F. One open door is all an attacker needs, and the scoring model reflects that.

What You'll Learn

  • Why a native sf plugin beats a standalone Python script for adoption
  • How the layered architecture handles parallel security checks
  • How the cache dependency system eliminates redundant API calls
  • How the configurable scoring model works

The Problem

The Python script worked, but it had adoption friction: teams had to maintain a separate Python environment, authenticate separately from the sf CLI, and follow manual install steps. For teams who already run sf daily, that barrier was enough to skip the audit entirely. Rewriting as a native sf plugin means a single sf plugins install command, the same authentication flow, and no Python dependency. The plugin lives inside the toolchain the team already trusts. TypeScript was the natural choice because the oclif framework and @salesforce/sf-plugins-core are both Node.js/TypeScript native, giving the plugin full access to the sf connection and org metadata without any bridging layer.

Quick Answer

Install with sf plugins install @cclabsnz/sf-audit, then run sf audit security --target-org <alias>. The plugin runs 22 parallel security checks across six layers: an API Client, a QueryRegistry that loads SOQL from JSON config, an AuditContext object shared across all checks, a CheckEngine that orchestrates execution and collects findings, a Scoring Layer that calculates the final health score, and Renderers that produce HTML, Markdown, or JSON output. A dependency-aware cache ensures that data fetched by one check (for example, Apex class bodies) is reused by any other check that needs it, eliminating redundant API calls. Risk weights live in config/scoring.json. You can tune them to your org's risk appetite without touching code. Any CRITICAL finding results in Grade F regardless of the numerical score.

We started with a Python script. It worked, but the friction was high: separate authentication, manual installs, and "ensure you have Python 3.11" requirements. When your team already runs sf, that's a barrier to adoption.

We rewrote it as a native sf plugin using TypeScript. Here is the architecture that made 22 parallel security checks practical.


Six Layers, One Job Each

The plugin is built on top of @salesforce/sf-plugins-core and oclif. We split the logic into six layers with a strict downward dependency rule:

  1. API Client: Wraps Salesforce REST and Tooling APIs.
  2. QueryRegistry: Loads SOQL definitions from JSON config. Checks don't write inline queries.
  3. AuditContext: A single object containing the connection, registry, and org metadata.
  4. CheckEngine: Orchestrates the execution of checks and collects findings.
  5. Scoring Layer: Assigns risk levels and calculates the final health score.
  6. Renderers: Implements AuditRenderer for HTML, Markdown, and JSON outputs.

Solving the Cache Problem

Running 22 checks means the same data is often requested multiple times. For example, HardcodedCredentialsCheck and ApexSharingCheck both need Apex class bodies.

We implemented a Dependency-Aware Cache:

  • populatesCache: A check declares what data it provides.
  • dependsOnCache: A check declares what data it needs.

The CheckEngine validates this order at startup. If ApexSharingCheck runs before the cache is populated, the plugin fails with a clear error before making a single API call. This ensures we never issue redundant scans for the same resources.


Scoring Without Hardcoding

Risk weights are decoupled from the code. They live in a config/scoring.json file, allowing users to tune the audit to their specific risk appetite:

{
  "riskScores": {
    "CRITICAL": 10,
    "HIGH": 7,
    "MEDIUM": 4,
    "LOW": 1,
    "INFO": 0
  }
}

Design Choice: Any CRITICAL finding results in an immediate Grade F, regardless of the numerical score. This reflects the reality that one open door is all an attacker needs.


Lessons Learned: The Abstraction Trap

Lesson Learned: Our QueryRegistry separated SOQL from logic by storing queries in JSON files. While it felt "clean," it made the code harder to navigate. In a future refactor, we would move the SOQL inline to the checks. The abstraction didn't pull its weight.


Extensible Output

The AuditRenderer interface is a single method: render(result: AuditResult): string.

Adding a new format (like a Slack-friendly summary or a CSV for auditors) requires zero changes to the core engine. You just implement the interface and register the new renderer.


Want to run this against your own org? Start with the usage guide.

If you are looking for a pattern where real-time collaboration replaces async spreadsheets, we used a similar approach when building the RACI alignment tool for this team.

Frequently Asked Questions

Q: Why TypeScript over Python for the sf plugin?

A: The sf CLI is built on the oclif framework, which is Node.js/TypeScript native. Writing the plugin in TypeScript means it installs via sf plugins install like any other sf plugin, uses the same authentication and connection model, and requires no separate runtime. Python would have needed a bridge layer and a separate install path, exactly the friction we were trying to remove.

Q: How does the cache dependency system work?

A: Each check declares populatesCache (what data it provides) and dependsOnCache (what data it requires). The CheckEngine validates this dependency order at startup and ensures cache-populating checks run first. Any check that depends on cached data (for example, ApexSharingCheck consuming Apex class bodies fetched by HardcodedCredentialsCheck) reads from the cache rather than issuing a second API call. If a dependency is missing, the engine fails with a clear error before making a single API call.

Q: Can I add my own security checks to the plugin?

A: Yes, the plugin is open source. Each check implements a common interface, declares its cache dependencies, and returns a list of findings. Adding a new check means implementing that interface and registering the check with the CheckEngine; no changes to the core layers are required.

Q: What Salesforce API permissions does the running user need?

A: The plugin uses the Salesforce REST API and Tooling API in read-only mode. The running user needs read access to the objects and metadata the checks query. In practice, a System Administrator profile covers all checks. A custom profile with read access to Users, Profiles, PermissionSets, Apex classes, Flows, and sharing objects will also work.

Key Takeaways

  • Layered Architecture: Six layers with a strict downward dependency rule. Each layer has one job, which makes the plugin easy to extend and debug without changes propagating across the entire codebase.
  • Cache Dependencies: The dependency-aware cache eliminates redundant API calls by letting checks declare what they produce and consume, with the engine enforcing the correct execution order at startup.
  • Scoring Model: Risk weights are decoupled from code and live in config/scoring.json, so any team can tune severity deductions and grade thresholds to match their org's specific risk appetite without modifying the plugin source.

What's Next?

Recommended Reading:

Action Items:

  1. Install the plugin: sf plugins install @cclabsnz/sf-audit
  2. Run your first audit: sf audit security --target-org <alias>
  3. Review the HTML report and prioritise Critical findings

Resources & References

  • [@cclabsnz/sf-audit on npm: npmjs.com/package/@cclabsnz/sf-audit]
  • [Salesforce CLI (sf) plugin development guide: developer.salesforce.com]
  • [oclif: The Open CLI Framework: oclif.io]
  • [TypeScript documentation: typescriptlang.org]