Skip to main content

Baselines

Every real Salesforce codebase has findings that the team has deliberately accepted: in-flight migrations, third-party code the team cannot rewrite, tactical workarounds that have a documented deprecation date. Running a scan against the whole project for the first time produces a list that mixes those accepted findings with the new ones the latest change introduced; the noise drowns the signal.

A baseline is the accepted-findings snapshot. After a baseline is in place, vulkro-sf scan --baseline baseline.json only reports net-new findings that did not exist in the baseline. The team gates CI on net-new only, which is the right shape: the build does not fail on debt the team has already accepted, and any new PR is held to the higher standard.

When you want a baseline

  • The first scan on a mature codebase. The full pass produces hundreds of findings; the engineering team's first job is to triage. A baseline freezes the current set; the next scan only surfaces new ones.
  • AppExchange resubmission gating. When the first submission to Salesforce Security Review fails on a known set of findings, the ISV fixes those, then baselines the new state. Subsequent revisions can only introduce net-new findings; anything from the baseline that re-appears is a regression and gates the build.
  • Consultancy engagement scoring. A consultancy that audits a customer's org at engagement start can baseline the findings on day one and report net-new vs net-fixed at engagement end as the engagement deliverable.

Generating a baseline

vulkro-sf scan writes the current scan's findings as a baseline file when you pass the --write-baseline flag:

vulkro-sf scan ./force-app --write-baseline baseline.json

This produces baseline.json containing one entry per finding: the rule ID, the file path, the line number, the message, and a stable baseline hash that fingerprints the finding's content. The baseline hash is line-tolerant: a fix or refactor that moves the finding up or down in the file does not invalidate the entry; only content-changing edits do.

Commit the baseline alongside the source. The recommended location is .vulkro/baseline.json at the repo root, so the same baseline ships with the project across machines and runners.

Scanning against a baseline

Subsequent scans pass --baseline:

vulkro-sf scan ./force-app --baseline .vulkro/baseline.json

The scan runs every detector as normal, then subtracts anything present in the baseline. The text output prints only the net-new findings; the SARIF, JSON, and HTML outputs do the same. The exit code follows the standard contract on the net-new set:

  • 0: scan completed and no net-new findings were reported.
  • 1: at least one net-new finding was reported.
  • 2: error.

A finding that disappears between the baseline and the current scan is a fix and does not gate. By default fixes are silent. Pass --report-fixed to print the closed findings; this is the right shape for the engagement-end deliverable.

vulkro-sf scan ./force-app \
--baseline .vulkro/baseline.json \
--report-fixed

Updating the baseline as you fix things

Over time the baseline should shrink. Two patterns work:

Pattern 1: fix the finding, drop the line

After fixing a finding and confirming the next scan no longer emits it, the baseline entry for that finding is stale (it points at a file location that no longer matches). Re-write the baseline:

vulkro-sf scan ./force-app --write-baseline baseline.json

This produces a fresh baseline reflecting the new current state. Commit it. The next PR scan now treats the shrunken set as the accepted floor.

Pattern 2: explicit drop list

When the team has agreed to actively work through a subset of the baseline, write that subset's rule IDs to a drop list and pass:

vulkro-sf scan ./force-app \
--baseline .vulkro/baseline.json \
--baseline-drop sf_metadata::guest_user_object_view_all

The baseline entries for that rule ID are excluded from the acceptance set; any remaining instance fires as net-new and gates CI.

AppExchange resubmission pattern

The end-to-end flow for an ISV using baselines to manage Security Review revisions:

  1. First submission. Run a full scan; do not use a baseline. Fix every finding the team intends to fix before submitting. Some are accepted as documented exceptions in the submission package.
  2. Submission fails with N findings. Salesforce returns a list of failing items.
  3. Fix the failures, then run vulkro-sf scan --write-baseline baseline.json to capture the now-clean state.
  4. Iterate on the package. Each subsequent revision is scanned with --baseline baseline.json. Only net-new findings gate the next submission.
  5. Re-submit. The clean run plus the readiness report (AppExchange readiness report) are the artifacts that go in the submission package.

The baseline becomes the contract: every revision must preserve the closed-finding set and not introduce regressions.

Consultancy engagement pattern

The end-to-end flow for a consultancy or internal IT-security team auditing a customer org:

  1. Day 1 (engagement start). Run a full scan on the customer's source plus a live-org pass (see source vs live org). Write the baseline:

    vulkro-sf scan ./force-app \
    --write-baseline customer-baseline.json
  2. Throughout the engagement. Every change the consultancy ships runs against the baseline. The team only sees new regressions, not the inherited backlog.

  3. Day 60 (engagement end). Run the engagement-end scan with --report-fixed:

    vulkro-sf scan ./force-app \
    --baseline customer-baseline.json \
    --report-fixed

    The output is a paired list: net-new findings (the regressions the team introduced, if any) plus net-fixed findings (the work the engagement closed). This is the deliverable: a measurable scope-of-work artifact that the customer signs off on.

Caveats

  • Baselines are file-and-content scoped, not line-scoped. A refactor that splits one file into two will produce new findings in the new file paths even though the underlying issue is unchanged. Update the baseline after such refactors.
  • Baselines do not justify the accepted findings. A long-lived baseline file with thousands of entries is a smell. The recommended discipline: keep the baseline small, and document the rationale for each retained finding in a sibling baseline-notes.md.
  • Severity changes do not invalidate baseline entries. If a detector tightens and bumps a Medium finding to High in a future Vulkro release, the existing baseline entry still matches and suppresses it. Re-write the baseline after a detector-release bump to surface the upgraded findings for re-triage.

Where to go next

  • CI/CD integration: the baseline-and-net-new pattern is the standard CI gate; the CI page shows the wiring per provider.
  • AppExchange readiness report: pair the baseline with the readiness HTML for the submission artifact.
  • Methodology: the master coverage matrix so you can decide which findings the baseline should retain and which the team intends to actively work through.