v0.1.0-draft AI Drafted

Salesforce Hardening Guide

Marketing Last updated: 2025-12-12

CRM platform security for MFA enforcement, Connected Apps, and Shield Event Monitoring

Salesforce Editions Covered: Enterprise, Unlimited, Performance (some controls require Shield add-on)


Overview

This guide provides comprehensive security hardening recommendations for Salesforce, organized by control category. Each recommendation includes both ClickOps (GUI-based) and Code (automation-based) implementation methods.

Intended Audience

  • Security engineers configuring Salesforce security controls
  • IT administrators managing Salesforce instances
  • GRC professionals assessing Salesforce compliance
  • Third-party risk managers evaluating integration security

How to Use This Guide

  • L1 (Baseline): Essential controls for all organizations
  • L2 (Hardened): Enhanced controls for security-sensitive environments
  • L3 (Maximum Security): Strictest controls for regulated industries

Scope

This guide covers Salesforce-specific security configurations. For infrastructure hardening (AWS, Azure where Salesforce runs), refer to CIS Benchmarks.


Table of Contents

  1. Authentication & Access Controls
  2. Network Access Controls
  3. OAuth & Connected App Security
  4. Data Security
  5. Monitoring & Detection
  6. Third-Party Integration Security

1. Authentication & Access Controls

1.1 Enforce Multi-Factor Authentication (MFA) for All Users

Profile Level: L1 (Baseline) CIS Controls: 6.3, 6.5 NIST 800-53: IA-2(1), IA-2(2)

Description

Require all Salesforce users to use MFA for authentication, eliminating single-factor authentication vulnerabilities.

Rationale

  • Attack Prevented: Credential stuffing, password spray, phished passwords
  • Incident Example: Okta support breach (2023) - attackers used stolen credentials without MFA

ClickOps Implementation

  1. Navigate to: Setup → Identity → Multi-Factor Authentication
  2. Enable: “Require Multi-Factor Authentication (MFA) for all direct UI logins”
  3. Configure allowed authenticator types:
    • ☑ Salesforce Authenticator (recommended)
    • ☑ TOTP-based apps (Google Authenticator, Authy)
    • ☐ SMS (NOT recommended - vulnerable to SIM swapping)
  4. Set enforcement date and communicate to users
  5. Verify in Login History: Setup → Security → Login History (check for MFA column)

Compliance Mappings

  • SOC 2: CC6.1 (Logical Access)
  • NIST 800-53: IA-2(1), IA-2(2)
  • PCI DSS: 8.3

Code Pack: API Script
hth-salesforce-1.01-enforce-mfa.sh View source on GitHub ↗
# Query active users who do not have MFA enabled
info "1.1 Querying active users without MFA..."
MFA_QUERY="SELECT Id, Username, Name, Profile.Name, IsActive, UserPreferencesDisableMFAPrompt FROM User WHERE IsActive = true"
USER_RESPONSE=$(sf_query "${MFA_QUERY}") || {
  fail "1.1 Failed to query user MFA status"
  increment_failed
  summary
  exit 0
}

TOTAL_USERS=$(echo "${USER_RESPONSE}" | jq '.totalSize // 0' 2>/dev/null)
info "1.1 Total active users: ${TOTAL_USERS}"

# Identify users with MFA prompt disabled (potential bypass)
MFA_DISABLED=$(echo "${USER_RESPONSE}" | jq '[.records[] | select(.UserPreferencesDisableMFAPrompt == true)]' 2>/dev/null || echo "[]")
DISABLED_COUNT=$(echo "${MFA_DISABLED}" | jq 'length' 2>/dev/null || echo "0")

if [ "${DISABLED_COUNT}" -gt 0 ]; then
  warn "1.1 Found ${DISABLED_COUNT} user(s) with MFA prompt disabled:"
  echo "${MFA_DISABLED}" | jq -r '.[] | "  - \(.Username) (\(.Name), Profile: \(.Profile.Name // "unknown"))"' 2>/dev/null || true
else
  pass "1.1 No users have MFA prompt disabled"
fi
# Verify org-wide session security settings require MFA
info "1.1 Checking org-wide session security level..."
SESSION_QUERY="SELECT Id, Name, SessionSecurityLevel FROM Profile WHERE Name IN ('System Administrator', 'Standard User')"
SESSION_RESPONSE=$(sf_query "${SESSION_QUERY}" 2>/dev/null || echo '{"records":[]}')

PROFILE_COUNT=$(echo "${SESSION_RESPONSE}" | jq '.totalSize // 0' 2>/dev/null)
if [ "${PROFILE_COUNT}" -gt 0 ]; then
  info "1.1 Profile session security levels:"
  echo "${SESSION_RESPONSE}" | jq -r '.records[] | "  - \(.Name): Session Level = \(.SessionSecurityLevel // "Standard")"' 2>/dev/null || true
else
  warn "1.1 Could not retrieve profile session settings"
fi
# Query permission sets with "Manage Multi-Factor Authentication" enabled
info "1.1 Checking permission sets for MFA management..."
PERM_QUERY="SELECT Id, Label, PermissionsManageMultiFactorInUi FROM PermissionSet WHERE PermissionsManageMultiFactorInUi = true"
PERM_RESPONSE=$(sf_query "${PERM_QUERY}" 2>/dev/null || echo '{"records":[]}')

PERM_COUNT=$(echo "${PERM_RESPONSE}" | jq '.totalSize // 0' 2>/dev/null)
if [ "${PERM_COUNT}" -gt 0 ]; then
  pass "1.1 Found ${PERM_COUNT} permission set(s) with MFA management enabled"
  echo "${PERM_RESPONSE}" | jq -r '.records[] | "  - \(.Label)"' 2>/dev/null || true
else
  warn "1.1 No permission sets with MFA management -- ensure MFA is enforced org-wide via Setup > Identity Verification"
fi

2. Network Access Controls

2.1 Restrict API Access via IP Allowlisting for Third-Party Integrations

Profile Level: L1 (Baseline) CIS Controls: 13.3, 13.6 NIST 800-53: AC-3, SC-7

Description

Configure Salesforce Network Access to restrict API calls from third-party integrations (like Gainsight, Drift, HubSpot) to their documented static egress IP addresses. This prevents compromised integrations from accessing your data from attacker-controlled infrastructure.

Rationale

Attack Prevented: Supply chain compromise via OAuth token theft

Real-World Incidents:

  • Gainsight Breach (November 2025): Attackers exfiltrated data from 200+ Salesforce orgs using stolen OAuth tokens from compromised Gainsight infrastructure
  • Salesloft/Drift Breach (August 2025): 700+ orgs compromised via stolen OAuth tokens
  • Okta Survival: Okta was targeted but protected because they had IP allowlisting configured

Why This Works: Even if integration’s OAuth tokens are stolen, attackers cannot use them from infrastructure outside the integration’s documented IP ranges.


Code Pack: API Script
hth-salesforce-2.01-restrict-api-ip-allowlisting.sh View source on GitHub ↗
# Query all configured Login IP Ranges across profiles
info "2.1 Querying Login IP Ranges by profile..."
IP_QUERY="SELECT Id, ProfileId, Profile.Name, StartAddress, EndAddress, Description FROM LoginIpRange ORDER BY Profile.Name"
IP_RESPONSE=$(sf_query "${IP_QUERY}") || {
  fail "2.1 Failed to query Login IP Ranges"
  increment_failed
  summary
  exit 0
}

RANGE_COUNT=$(echo "${IP_RESPONSE}" | jq '.totalSize // 0' 2>/dev/null)

if [ "${RANGE_COUNT}" -gt 0 ]; then
  pass "2.1 Found ${RANGE_COUNT} Login IP Range(s) configured"
  echo "${IP_RESPONSE}" | jq -r '.records[] | "  - Profile: \(.Profile.Name // "unknown") | \(.StartAddress) - \(.EndAddress) | \(.Description // "no description")"' 2>/dev/null || true
else
  warn "2.1 No Login IP Ranges configured -- API access is unrestricted by IP"
  warn "2.1 NIST SC-7: Network boundary protection requires IP-based access controls"
fi
# Query Trusted IP Ranges (org-wide network access)
info "2.1 Checking org-wide Trusted IP Ranges..."
TRUSTED_QUERY="SELECT Id, StartAddress, EndAddress, Description FROM SecuritySettings"
# Trusted IP ranges are in Network Access -- query via Setup API
NETWORK_RESPONSE=$(sf_tooling_query "SELECT Id, StartAddress, EndAddress, Description FROM NetworkAccess" 2>/dev/null || echo '{"records":[]}')

TRUSTED_COUNT=$(echo "${NETWORK_RESPONSE}" | jq '.totalSize // 0' 2>/dev/null)

if [ "${TRUSTED_COUNT}" -gt 0 ]; then
  info "2.1 Found ${TRUSTED_COUNT} Trusted IP Range(s) (org-wide):"
  echo "${NETWORK_RESPONSE}" | jq -r '.records[] | "  - \(.StartAddress) - \(.EndAddress) | \(.Description // "no description")"' 2>/dev/null || true
else
  warn "2.1 No org-wide Trusted IP Ranges configured"
fi
# Audit recent login IPs to identify unexpected sources
info "2.1 Auditing recent login source IPs (last 100 logins)..."
LOGIN_QUERY="SELECT Id, UserId, LoginTime, SourceIp, Status, Application, LoginType FROM LoginHistory ORDER BY LoginTime DESC LIMIT 100"
LOGIN_RESPONSE=$(sf_query "${LOGIN_QUERY}" 2>/dev/null || echo '{"records":[]}')

LOGIN_COUNT=$(echo "${LOGIN_RESPONSE}" | jq '.totalSize // 0' 2>/dev/null)
if [ "${LOGIN_COUNT}" -gt 0 ]; then
  # Summarize unique source IPs
  UNIQUE_IPS=$(echo "${LOGIN_RESPONSE}" | jq -r '[.records[].SourceIp] | unique | .[]' 2>/dev/null || true)
  IP_COUNT=$(echo "${UNIQUE_IPS}" | grep -c . 2>/dev/null || echo "0")
  info "2.1 Found ${IP_COUNT} unique source IP(s) in last ${LOGIN_COUNT} logins:"
  echo "${UNIQUE_IPS}" | while read -r ip; do
    COUNT=$(echo "${LOGIN_RESPONSE}" | jq "[.records[] | select(.SourceIp == \"${ip}\")] | length" 2>/dev/null || echo "?")
    echo "  - ${ip} (${COUNT} logins)"
  done

  # Flag failed logins from unexpected IPs
  FAILED=$(echo "${LOGIN_RESPONSE}" | jq '[.records[] | select(.Status == "Failed")]' 2>/dev/null || echo "[]")
  FAILED_COUNT=$(echo "${FAILED}" | jq 'length' 2>/dev/null || echo "0")
  if [ "${FAILED_COUNT}" -gt 0 ]; then
    warn "2.1 Found ${FAILED_COUNT} failed login(s) -- review source IPs:"
    echo "${FAILED}" | jq -r '.[] | "  - \(.SourceIp) at \(.LoginTime) via \(.Application // "unknown")"' 2>/dev/null || true
  fi
else
  warn "2.1 No login history available -- verify API token has login history read permissions"
fi
Code Pack: DB Query
hth-salesforce-2.01-detect-gainsight-blocks.sql View source on GitHub ↗
-- Query for blocked Gainsight login attempts
SELECT Id, LoginTime, SourceIp, Status, Application
FROM LoginHistory
WHERE Application = 'Gainsight'
  AND Status = 'Failed'
  AND LoginTime = LAST_N_DAYS:7
Code Pack: Sigma Detection Rule
hth-salesforce-2.01-ip-allowlist-changed.yml View source on GitHub ↗
detection:
    selection:
        EventType: 'SetupAuditTrail'
        Action|contains:
            - 'insertedLoginIpRange'
            - 'deletedLoginIpRange'
            - 'updatedLoginIpRange'
            - 'insertedNetworkAccess'
            - 'deletedNetworkAccess'
    condition: selection
fields:
    - CreatedDate
    - CreatedByContext
    - Display
    - Section
    - DelegateUser
    - ResponsibleNamespacePrefix

2.1.1 IP Allowlisting: Restricting Gainsight

Prerequisites

  • Salesforce Enterprise Edition or higher
  • Gainsight’s current static egress IP addresses
  • System Administrator access

Gainsight IP Addresses

As of 2025-12-12, Gainsight uses these production egress IPs:

  • 35.166.202.113/32
  • 52.35.87.209/32
  • 34.221.135.142/32

⚠️ Verify before implementing: Contact your Gainsight CSM or check Gainsight IP Documentation

ClickOps Implementation

Step 1: Navigate to Network Access

  1. Setup (gear icon) → Quick Find: “Network Access” → Network Access

Step 2: Add Gainsight IP Ranges For each IP address:

  1. Click “New” in Trusted IP Ranges section
  2. Enter:
    • Start IP Address: 35.166.202.113
    • End IP Address: 35.166.202.113
    • Description: Gainsight Production 1 - verified 2025-12-12
  3. Click “Save”
  4. Repeat for remaining IPs (52.35.87.209, 34.221.135.142)

Step 3: Test Integration

  1. Trigger Gainsight manual sync
  2. Verify data flows correctly
  3. Check Login History for blocked attempts: Setup → Security → Login History

Time to Complete: ~10 minutes

Monitoring & Maintenance

Quarterly Review Checklist:

  • Verify Gainsight IPs haven’t changed (contact CSM or check documentation)
  • Update description fields with new verification date
  • Review Event Monitoring logs for blocked attempts
  • Test integration after any changes

Alert Configuration: If using Salesforce Shield Event Monitoring, see the DB Query in the Code Pack for section 2.1 above.

Operational Impact

  • User Experience: None (users don’t interact with integration directly)
  • Integration Functionality: Low risk if IPs verified with vendor
  • Rollback: Easy - remove IP ranges from trusted list

Compliance Mappings

  • SOC 2: CC6.6 (Boundary Protection)
  • NIST 800-53: AC-3, SC-7, SC-7(5)
  • ISO 27001: A.8.3 (Supplier relationships)

2.1.2 IP Allowlisting: Restricting Drift

Drift IP Addresses

As of 2025-12-12:

  • 52.2.219.12/32
  • 54.196.47.40/32
  • 54.82.90.31/32

Source: Drift IP Allowlist Documentation

Implementation: Follow same process as Gainsight (Section 2.1.1), replacing IP addresses.


2.1.3 IP Allowlisting: Restricting HubSpot

HubSpot IP Addresses

HubSpot publishes their IP ranges at: https://knowledge.hubspot.com/integrations/what-are-hubspot-s-ip-addresses

Note: HubSpot has a larger IP range and may change more frequently. Consider:

  • More frequent verification (monthly vs quarterly)
  • Monitoring HubSpot status page for infrastructure changes

2.2 Restrict Login Hours by Profile

Profile Level: L2 (Hardened)

Description

Limit when users can log into Salesforce based on their role/profile, reducing attack surface during off-hours.

ClickOps Implementation

  1. Setup → Users → Profiles → [Select Profile]
  2. Click “Login Hours” button
  3. Configure allowed hours per day of week
  4. Save and test with affected users

Use Cases

  • Restrict contractor access to business hours only
  • Limit admin account login times to reduce exposure
  • Geographic-based restrictions (e.g., US-only profiles during US business hours)

3. OAuth & Connected App Security

3.1 Audit and Reduce OAuth Scopes for Connected Apps

Profile Level: L1 (Baseline) CIS Controls: 6.2 (Least Privilege) NIST 800-53: AC-6

Description

Review all Connected Apps (third-party integrations) and ensure they only have minimum required OAuth scopes. Over-permissioned apps increase breach impact.

Rationale

Attack Impact: When Gainsight was breached, attackers had full OAuth scope, allowing complete data exfiltration. Scoped permissions would have limited damage.

ClickOps Implementation

Step 1: Audit Current Connected Apps

  1. Setup → Apps → Connected Apps → Manage Connected Apps
  2. Review each app’s “Selected OAuth Scopes”
  3. Document current scopes and business justification

Step 2: Identify Over-Permissioned Apps Look for apps with:

  • full - Complete access (almost never needed)
  • api - Full API access (often too broad)
  • refresh_token, offline_access - Persistent access (risk if breached)

Step 3: Reduce Scopes For each over-permissioned app:

  1. Click app name → Edit Policies
  2. Modify Selected OAuth Scopes to minimum required:
    • Example: Change full to specific scopes like chatter_api, custom_permissions
  3. Save
  4. Test integration to ensure functionality maintained

Step 4: Enable OAuth App Approval

  1. Setup → Security → Session Settings
  2. Enable: “Require user authorization for OAuth flows”
  3. This forces users to explicitly approve OAuth apps
Integration Type Recommended Scopes Avoid
Customer Success (Gainsight) api, custom_permissions, specific objects full, refresh_token with long expiry
Marketing (HubSpot, Drift) api, chatter_api, limited objects full, manage_users
Support (Zendesk, Intercom) api, chatter_api, Case object only full, access to all objects
Analytics (Tableau) api, read-only specific objects Write access, full

Compliance Mappings

  • SOC 2: CC6.2 (Least Privilege)
  • NIST 800-53: AC-6, AC-6(1)
  • ISO 27001: A.9.2.3

Code Pack: API Script
hth-salesforce-3.01-audit-connected-app-scopes.sh View source on GitHub ↗
# List all Connected Applications with OAuth settings
info "3.1 Querying Connected Applications..."
APP_QUERY="SELECT Id, Name, CreatedDate, CreatedBy.Name, LastModifiedDate FROM ConnectedApplication ORDER BY Name"
APP_RESPONSE=$(sf_tooling_query "${APP_QUERY}") || {
  fail "3.1 Failed to query Connected Applications"
  increment_failed
  summary
  exit 0
}

APP_COUNT=$(echo "${APP_RESPONSE}" | jq '.totalSize // 0' 2>/dev/null)

if [ "${APP_COUNT}" -gt 0 ]; then
  info "3.1 Found ${APP_COUNT} Connected Application(s):"
  echo "${APP_RESPONSE}" | jq -r '.records[] | "  - \(.Name) (Created: \(.CreatedDate), By: \(.CreatedBy.Name // "unknown"))"' 2>/dev/null || true
else
  info "3.1 No Connected Applications found"
fi
# Audit active OAuth access tokens to identify over-permissioned integrations
info "3.1 Querying active OAuth tokens..."
TOKEN_QUERY="SELECT Id, AppName, UserId, CreatedDate, LastUsedDate FROM OAuthToken ORDER BY LastUsedDate DESC NULLS LAST"
TOKEN_RESPONSE=$(sf_query "${TOKEN_QUERY}" 2>/dev/null || echo '{"records":[],"totalSize":0}')

TOKEN_COUNT=$(echo "${TOKEN_RESPONSE}" | jq '.totalSize // 0' 2>/dev/null)

if [ "${TOKEN_COUNT}" -gt 0 ]; then
  info "3.1 Found ${TOKEN_COUNT} active OAuth token(s):"
  echo "${TOKEN_RESPONSE}" | jq -r '.records[] | "  - \(.AppName // "unnamed") (Last Used: \(.LastUsedDate // "never"), Created: \(.CreatedDate))"' 2>/dev/null || true

  # Flag tokens not used in 90+ days
  STALE_QUERY="SELECT Id, AppName, UserId, CreatedDate, LastUsedDate FROM OAuthToken WHERE LastUsedDate < LAST_N_DAYS:90"
  STALE_RESPONSE=$(sf_query "${STALE_QUERY}" 2>/dev/null || echo '{"records":[],"totalSize":0}')
  STALE_COUNT=$(echo "${STALE_RESPONSE}" | jq '.totalSize // 0' 2>/dev/null)

  if [ "${STALE_COUNT}" -gt 0 ]; then
    warn "3.1 Found ${STALE_COUNT} stale OAuth token(s) unused for 90+ days -- consider revoking:"
    echo "${STALE_RESPONSE}" | jq -r '.records[] | "  - \(.AppName // "unnamed") (Last Used: \(.LastUsedDate // "never"))"' 2>/dev/null || true
  else
    pass "3.1 No stale OAuth tokens found (all used within 90 days)"
  fi
else
  info "3.1 No active OAuth tokens found"
fi
# Check Connected App OAuth policies (admin approval, IP relaxation)
info "3.1 Checking Connected App OAuth policies..."
POLICY_QUERY="SELECT Id, Name, OptionsAllowAdminApprovedUsersOnly, OptionsRefreshTokenValidityMetric FROM ConnectedApplication"
POLICY_RESPONSE=$(sf_tooling_query "${POLICY_QUERY}" 2>/dev/null || echo '{"records":[],"totalSize":0}')

POLICY_COUNT=$(echo "${POLICY_RESPONSE}" | jq '.totalSize // 0' 2>/dev/null)

if [ "${POLICY_COUNT}" -gt 0 ]; then
  # Flag apps that do NOT require admin pre-authorization
  OPEN_APPS=$(echo "${POLICY_RESPONSE}" | jq '[.records[] | select(.OptionsAllowAdminApprovedUsersOnly != true)]' 2>/dev/null || echo "[]")
  OPEN_COUNT=$(echo "${OPEN_APPS}" | jq 'length' 2>/dev/null || echo "0")

  if [ "${OPEN_COUNT}" -gt 0 ]; then
    warn "3.1 Found ${OPEN_COUNT} Connected App(s) not requiring admin pre-authorization:"
    echo "${OPEN_APPS}" | jq -r '.[] | "  - \(.Name) -- set OAuth policy to 'Admin approved users are pre-authorized'"' 2>/dev/null || true
  else
    pass "3.1 All Connected Apps require admin pre-authorization"
  fi
else
  info "3.1 No Connected App policy data available"
fi
Code Pack: Sigma Detection Rule
hth-salesforce-3.01-connected-app-scope-modified.yml View source on GitHub ↗
detection:
    selection_app_change:
        EventType: 'SetupAuditTrail'
        Action|contains:
            - 'createdConnectedApp'
            - 'updatedConnectedApp'
            - 'deletedConnectedApp'
            - 'changedConnectedAppOauth'
            - 'changedConnectedAppAttributes'
    selection_oauth_policy:
        EventType: 'SetupAuditTrail'
        Action|contains:
            - 'changedOauthPolicy'
            - 'updatedOauthToken'
    condition: selection_app_change or selection_oauth_policy
fields:
    - CreatedDate
    - CreatedByContext
    - Display
    - Section
    - DelegateUser
    - ResponsibleNamespacePrefix

3.2 Enable Connected App Session-Level Security

Profile Level: L2 (Hardened)

Description

Configure Connected Apps to inherit session security policies (IP restrictions, timeout) from user’s profile.

ClickOps Implementation

  1. Setup → Apps → Connected Apps → [App Name]
  2. Edit Policies
  3. Session Timeout: Set to “2 hours” or less (not “Never expires”)
  4. Refresh Token Policy: “Expire after 30 days” (not “Refresh token valid indefinitely”)
  5. Enable: “Enforce IP restrictions”

4. Data Security

4.1 Enable Field-Level Encryption for Sensitive Data

Profile Level: L2 (Hardened) Requires: Salesforce Shield

Description

Encrypt sensitive fields (SSN, credit card, health data) at rest using Salesforce Shield Platform Encryption.

ClickOps Implementation

  1. Setup → Security → Platform Encryption
  2. Generate tenant secret (store securely!)
  3. Select fields to encrypt:
    • Custom fields marked as sensitive
    • Standard fields: SSN, Credit Card, etc.
  4. Enable encryption per field
  5. Test: Encrypted fields show lock icon

Limitations

  • Encrypted fields cannot be used in:
    • WHERE clauses (except equality)
    • ORDER BY
    • Formula fields (in some cases)
  • Requires Shield add-on (~$25/user/month)

5. Monitoring & Detection

5.1 Enable Event Monitoring for API Anomalies

Profile Level: L1 (Baseline) Requires: Salesforce Shield or Event Monitoring add-on

Description

Enable Salesforce Event Monitoring to detect anomalous API usage patterns that could indicate compromised integrations.

ClickOps Implementation

  1. Setup → Event Monitoring → Event Manager
  2. Enable these event types:
    • API (all API calls)
    • Login (authentication events)
    • URI (page views)
    • Report Export (data exfiltration indicator)
  3. Configure storage: EventLogFile (24hr) or Event Monitoring Analytics (30 days)

Detection Queries

Code Pack: API Script
hth-salesforce-5.01-enable-event-monitoring.sh View source on GitHub ↗
# Query available EventLogFile types to verify Event Monitoring is active
info "5.1 Checking EventLogFile availability..."
LOG_QUERY="SELECT Id, EventType, LogDate, LogFileLength FROM EventLogFile ORDER BY LogDate DESC LIMIT 50"
LOG_RESPONSE=$(sf_query "${LOG_QUERY}") || {
  fail "5.1 Failed to query EventLogFile -- Event Monitoring may not be enabled"
  fail "5.1 Enable via Setup > Event Monitoring Settings or purchase Salesforce Shield"
  increment_failed
  summary
  exit 0
}

LOG_COUNT=$(echo "${LOG_RESPONSE}" | jq '.totalSize // 0' 2>/dev/null)

if [ "${LOG_COUNT}" -gt 0 ]; then
  pass "5.1 Event Monitoring is active -- found ${LOG_COUNT} recent log file(s)"

  # Summarize event types available
  EVENT_TYPES=$(echo "${LOG_RESPONSE}" | jq -r '[.records[].EventType] | unique | sort | .[]' 2>/dev/null || true)
  TYPE_COUNT=$(echo "${EVENT_TYPES}" | grep -c . 2>/dev/null || echo "0")
  info "5.1 ${TYPE_COUNT} event type(s) available:"
  echo "${EVENT_TYPES}" | while read -r etype; do
    echo "  - ${etype}"
  done
else
  warn "5.1 No EventLogFile records found -- Event Monitoring may not be enabled"
  warn "5.1 Enable via Setup > Event Monitoring Settings"
fi
# Look for API-related event types indicating anomaly detection
info "5.1 Checking for API anomaly event types..."
API_LOG_QUERY="SELECT Id, EventType, LogDate, LogFileLength FROM EventLogFile WHERE EventType IN ('ApiTotalUsage', 'API', 'RestApi', 'BulkApi', 'Login', 'LoginAs') ORDER BY LogDate DESC LIMIT 20"
API_LOG_RESPONSE=$(sf_query "${API_LOG_QUERY}" 2>/dev/null || echo '{"records":[],"totalSize":0}')

API_LOG_COUNT=$(echo "${API_LOG_RESPONSE}" | jq '.totalSize // 0' 2>/dev/null)

if [ "${API_LOG_COUNT}" -gt 0 ]; then
  pass "5.1 Found ${API_LOG_COUNT} API-related event log(s)"
  echo "${API_LOG_RESPONSE}" | jq -r '.records[] | "  - \(.EventType) on \(.LogDate) (\(.LogFileLength) bytes)"' 2>/dev/null || true
else
  warn "5.1 No API-related event logs found -- enable API event types in Event Monitoring"
fi
# Audit recent failed login attempts for suspicious activity
info "5.1 Auditing recent failed login attempts..."
FAILED_QUERY="SELECT Id, LoginTime, SourceIp, Status, Application, UserId, LoginType FROM LoginHistory WHERE Status = 'Failed' ORDER BY LoginTime DESC LIMIT 50"
FAILED_RESPONSE=$(sf_query "${FAILED_QUERY}" 2>/dev/null || echo '{"records":[],"totalSize":0}')

FAILED_COUNT=$(echo "${FAILED_RESPONSE}" | jq '.totalSize // 0' 2>/dev/null)

if [ "${FAILED_COUNT}" -gt 0 ]; then
  warn "5.1 Found ${FAILED_COUNT} recent failed login attempt(s):"

  # Group by source IP for anomaly detection
  UNIQUE_FAIL_IPS=$(echo "${FAILED_RESPONSE}" | jq -r '[.records[].SourceIp] | unique | .[]' 2>/dev/null || true)
  echo "${UNIQUE_FAIL_IPS}" | while read -r ip; do
    COUNT=$(echo "${FAILED_RESPONSE}" | jq "[.records[] | select(.SourceIp == \"${ip}\")] | length" 2>/dev/null || echo "?")
    echo "  - ${ip}: ${COUNT} failed attempt(s)"
  done

  # Flag IPs with 5+ failures (brute force indicator)
  echo "${UNIQUE_FAIL_IPS}" | while read -r ip; do
    COUNT=$(echo "${FAILED_RESPONSE}" | jq "[.records[] | select(.SourceIp == \"${ip}\")] | length" 2>/dev/null || echo "0")
    if [ "${COUNT}" -ge 5 ]; then
      warn "5.1 ALERT: ${ip} has ${COUNT} failed logins -- possible brute force"
    fi
  done
else
  pass "5.1 No recent failed login attempts found"
fi
Code Pack: Sigma Detection Rule
hth-salesforce-5.01-bulk-data-export-detected.yml View source on GitHub ↗
detection:
    selection_bulk_api:
        EventType:
            - 'BulkApi'
            - 'ApiBulkApiHard'
    selection_data_export:
        EventType: 'DataExport'
    selection_large_query:
        EventType:
            - 'API'
            - 'RestApi'
        ROWS_PROCESSED|gte: 10000
    condition: selection_bulk_api or selection_data_export or selection_large_query
fields:
    - EventDate
    - Username
    - SourceIp
    - ROWS_PROCESSED
    - ENTITY_NAME
    - CLIENT_NAME
    - REQUEST_STATUS

Alert Configuration

Using Salesforce Shield:

  1. Create custom report with anomaly query
  2. Subscribe to report with alert threshold
  3. Configure email/Slack notification

Using Third-Party SIEM: Export EventLogFile daily to:

  • Splunk
  • Datadog
  • Sumo Logic
  • AWS Security Lake

6. Third-Party Integration Security

6.1 Integration Risk Assessment Matrix

Before allowing any third-party integration, assess risk:

Risk Factor Low Medium High
Data Access Read-only, limited objects Read most objects Write access, full API
OAuth Scopes Specific scopes only api scope full scope
Session Duration <2 hours 2-8 hours >8 hours, refresh tokens
IP Restriction Static IPs, allowlisted Some static IPs Dynamic IPs, no allowlist
Vendor Security SOC 2 Type II, recent audit SOC 2 Type I No SOC 2

Decision Matrix:

  • 0-5 points: Approve with standard controls
  • 6-10 points: Approve with enhanced monitoring
  • 11-15 points: Require additional security measures or reject

Gainsight (Customer Success Platform)

Data Access: High (needs Account, Contact, Case, Custom Objects) Recommended Controls:

  • ✅ IP allowlisting (Section 2.1.1)
  • ✅ Reduce OAuth scopes from full to api + specific objects
  • ✅ Enable Event Monitoring for bulk queries
  • ✅ 30-day refresh token expiration

Drift (Marketing/Chat Platform)

Data Access: Medium (needs Lead, Contact, Account) Recommended Controls:

  • ✅ IP allowlisting (Section 2.1.2)
  • ✅ Read-only access to Lead/Contact
  • ✅ Restrict to marketing team profile
  • ⚠️ Note: Drift was breached in 2025 - high-risk integration

HubSpot (Marketing Automation)

Data Access: Medium-High Recommended Controls:

  • ✅ IP allowlisting (Section 2.1.3) with monthly verification
  • ✅ Bidirectional sync monitoring (alert on unexpected write operations)
  • ✅ Field-level restrictions (don’t sync SSN, financial data)

7. Compliance Quick Reference

SOC 2 Trust Services Criteria Mapping

Control ID Salesforce Control Guide Section
CC6.1 MFA for all users 1.1
CC6.2 OAuth scope reduction 3.1
CC6.6 IP allowlisting 2.1
CC7.2 Event Monitoring 5.1

NIST 800-53 Rev 5 Mapping

Control Salesforce Control Guide Section
AC-3 IP restrictions 2.1
AC-6 Least privilege OAuth 3.1
IA-2(1) MFA enforcement 1.1
AU-6 Event monitoring 5.1

Appendix A: Edition Compatibility

Control Professional Enterprise Unlimited Performance Shield Required
MFA
IP Allowlisting
OAuth Scoping
Event Monitoring Add-on Add-on Add-on
Field Encryption

Appendix B: References

Official Salesforce Documentation:

API & Developer Resources:

Trust & Compliance:

Integration Vendor IP Documentation:

Supply Chain Incident Reports:

Security Incidents:

  • Salesloft/Drift OAuth Supply Chain Attack (August-September 2025): Attackers compromised Salesloft/Drift infrastructure and exfiltrated OAuth tokens, affecting 700+ Salesforce orgs. Gainsight breach separately impacted 200+ orgs via stolen OAuth tokens (November 2025). IP allowlisting proved effective – Okta was targeted but protected because they had IP restrictions configured.

Changelog

Date Version Maturity Changes Author
2025-12-12 0.1.0 draft Initial Salesforce hardening guide with focus on integration security Claude Code (Opus 4.5)

Next Steps:

  1. Review your current Salesforce configuration against L1 (Baseline) controls
  2. Implement IP allowlisting for high-risk integrations (Gainsight, Drift, HubSpot)
  3. Audit Connected App OAuth scopes and reduce over-permissions
  4. Enable Event Monitoring for API anomaly detection
  5. Establish quarterly review process for integration security

Questions or Improvements?