Skip to main content

Signed rules-bundle distribution

Salesforce ships Agentforce metadata changes on a multi-week cadence, and the third-party reviewers track new managed-package customer- verification actions, new Trusted URL wildcards, and new PII surfaces as they appear. Vulkro for Salesforce v0.1.3 ships a bundle channel that lets the detection rules keep up without requiring a binary release for every catalog change.

How it works

The scanner consults a knowledge bundle every time it runs the Agentforce / planner / topic / action checks (AP-061..AP-070). The bundle is a single JSON file with the catalog data the rules need:

  • Standard managed-package customer-verification actions and their required outputs.
  • Word-count thresholds for topic master labels, descriptions, and scope strings.
  • The per-planner excessive-topic threshold.
  • File-name patterns that count as test files for an agent / topic / action.
  • PII-bearing free-text fields the ForcedLeak class can flow through.
  • Trusted URL wildcard patterns the platform has removed from the default allowlist.

Every binary ships with a seed copy of this bundle baked in. The seed is the canonical fallback: a scan never blocks on a missing or unreachable bundle, and an air-gapped install always has a usable catalog out of the box.

On a network-connected install, the scanner asks the seed if it has checked for an update in the last 24 hours. If not, it makes a single HTTPS GET to https://dist.vulkro.com/sf/rules/agentforce.bundle.json plus a sibling GET to agentforce.bundle.sig. The signature is verified against an ed25519 public key baked into the binary at compile time. A signature mismatch is treated as a tamper event: the fetched bundle is rejected and the seed catalog is used.

Why it's signed

The threat model is "an attacker who controls the CDN can poison the catalog." A bundle without a signature would let that attacker swap in their own catalog: they could quietly add their domain to the trusted_url_wildcard_blocklist removed list to silence a finding, or remove a PII field from the catalog to silence a different one.

The signature defends against that exactly. The public key is binary-pinned: an attacker who compromises the CDN cannot also swap the verification key, because that key lives in the customer's local binary. Key rotation requires a new binary release that bumps the pinned constant, which is the right control: a key compromise should not be self-recoverable from a bundle update.

The private signing key never ships and never travels. It lives only on the release-signing host. The signing CLI (vulkro-sign-rules-bundle) is operator-side, never shipped to customers.

What the rules read from the bundle

FindingCatalog field
AP-061 (PII free-text into agent)pii_free_text_fields
AP-062 (over-broad Trusted URL)trusted_url_wildcard_blocklist
AP-066 (missing customer verification)standard_verification_actions
AP-067 (missing output assignments)standard_verification_actions.required_outputs
AP-068 (insufficient topic instructions)topic_word_thresholds
AP-069 (excessive topics)excessive_topics_threshold
AP-070 (untested agent action)test_file_patterns

AP-063, AP-064, AP-018/024/042/044, AP-056..AP-058, and AP-060 do not consult the bundle; they are pure-code or pure-metadata heuristics and ship in the binary.

Operator controls

The fetch path honours four environment variables:

  • VULKRO_SF_OFFLINE=1 skips the fetch entirely. The scanner uses the seed catalog regardless of whether a refresh is due. Recommended for air-gapped CI lanes and for customers who want zero outbound HTTPS from the scanner.
  • VULKRO_SF_RULES_BUNDLE_OVERRIDE=<path> sideloads a local JSON file as the catalog instead of fetching. Useful when testing a candidate bundle before publishing.
  • VULKRO_SF_RULES_BUNDLE_URL=<base> overrides the base URL. Default is https://dist.vulkro.com/sf/rules. Useful for air-gapped mirrors that proxy the production CDN.
  • VULKRO_SF_RULES_BUNDLE_CACHE_DIR=<path> overrides the on-disk cache directory. Default is ~/.vulkro/rules-bundle/.
  • VULKRO_SF_RULES_BUNDLE_MIN_REFRESH_SECS=<n> overrides the 24-hour refresh cadence. Clamped to [60, 604800] (1 minute to 7 days). To disable updates entirely, set VULKRO_SF_OFFLINE=1 rather than pushing this value to the ceiling.

When the seed wins

The seed catalog wins (the scanner ignores the fetched bundle) when:

  • VULKRO_SF_OFFLINE=1 is set.
  • The fetch fails (DNS error, TLS handshake fail, 404, 5xx).
  • The signature does not verify against the pinned public key.
  • The JSON does not parse (#[serde(deny_unknown_fields)] is on, so a bundle that adds an unknown top-level key is rejected).
  • The cache directory cannot be created or written to.

In each case the scanner emits a single INFO-level line on stderr naming the failure mode (remote Agentforce bundle unavailable (...); using seed) so an operator can decide whether to investigate. An offline-by-policy install logs nothing; a SHA mismatch logs the mismatch and uses the seed.

What the wire format looks like

GET https://dist.vulkro.com/sf/rules/agentforce.bundle.json
GET https://dist.vulkro.com/sf/rules/agentforce.bundle.sig

The .json is a UTF-8 JSON document conforming to the AgentforceKnowledge schema. The .sig is a raw 64-byte detached ed25519 signature over the JSON's bytes. No base64, no hex, no envelope: the bytes are exactly what the customer-side fetcher reads.

The same shape ships in v0.1.3 binaries today. Future schema additions will use #[serde(default)] so an older customer install can still parse a newer bundle without rejecting it.

Where to go next