# Rules: access, integrations and reporting

ACC-01 to ACC-06, INT-01 to INT-07 and REP-01 to REP-07. Severity, classes and the FAIL downgrade rule are defined at the top of [rules-data-model.md](rules-data-model.md). Checked on 2026-11-06. Record role labels (user-07, agency-01, integration-billing) in shared reports, never names or emails.

## ACC-01 Privileged access is small and personal (A, automation gate)

- FAIL: any privileged account (HubSpot Super Admin; Salesforce Modify All Data, View All Data or System Administrator; Attio admin) belongs to a former employee, an agency, a shared or generic login, or an integration identity without documented need. This holds whatever the count.
- WARN: privileged accounts above the larger of 3 and 10% of active users.
- Evidence: HubSpot users list with the Super Admin flag. Salesforce `SELECT AssigneeId, PermissionSet.Name FROM PermissionSetAssignment WHERE (PermissionSet.PermissionsModifyAllData = true OR PermissionSet.PermissionsViewAllData = true) AND Assignee.IsActive = true`. Attio `list-workspace-members` with access level admin.
- Script: `users[]` (active, privileged, account_type, documented_need).

## ACC-02 Integrations use dedicated, least-privilege identities (A, automation gate)

- FAIL: an integration authenticates as a named person's own user; an integration identity holds Super Admin, Modify All Data or workspace admin without documented need.
- WARN: a dedicated identity whose scopes exceed what it writes (a private app with write scopes on objects it only reads).
- Evidence: HubSpot connected apps, legacy private apps with creator and scopes, service keys. Salesforce integration users, the User Access Summary per integration user, `OauthToken` by user. Attio token scopes through Identify.
- Script: `integrations[].identity_type`. An integration whose identity type is not recorded leaves the rule NOT CHECKED; it never passes by default, and beside a WARN on another integration it keeps the automation verdict at Not assessable.
- Agent adds: an integration on a personal account breaks when that person leaves and hides the real writer in history. Salesforce: prefer a user on the Salesforce Integration user license with the Minimum Access - API Only Integrations profile and access granted by permission sets, one user per integration; an integration user with System Administrator and no documented need is FAIL.

## ACC-03 Departed and inactive users do not own live work (B)

- FAIL: deactivated or departed users own open deals, open tickets or routing-eligible records, including a departed person whose account is still active (`account_type` former_employee), who can still log in.
- WARN: paid seats with no login in 90 days.
- Evidence: HubSpot owners including archived owners, open deal counts by owner. Salesforce `SELECT COUNT() FROM Opportunity WHERE IsClosed = false AND Owner.IsActive = false` and `User.LastLoginDate`. Attio suspended members who own records or list entries.

## ACC-04 Bulk destructive rights are limited (B, never FAIL)

- WARN: more than 20% of active non-admin users can bulk delete, merge or export all records.
- Evidence: HubSpot user permission exports or captures; Salesforce object Modify All, Export Reports and Weekly Data Export permissions; Attio full-access list members.

## ACC-05 Sensitive fields and records are restricted on purpose (A; B when exposure is possible but not confirmed)

- FAIL: a field the privacy or finance owner marked restricted is readable by roles outside the approved list, including through a formula or roll-up field or Salesforce View All Fields; an object holding restricted records (opportunities with amounts, contracts) is readable by roles outside the approved list through org-wide defaults, sharing rules or a View access of all records; an Attio Free workspace holds data that must be restricted (Free workspaces are visible and editable by all users).
- WARN: no list of restricted fields exists; property restrictions or page layouts are the only control; Account or Opportunity org-wide defaults are Public Read/Write with no dated decision.
- Evidence, fields: HubSpot property access settings and Sensitive Data settings (both Enterprise). Salesforce Field Access Summary per restricted field, View All Fields grants, formula fields that reference restricted inputs.
- Evidence, records: Salesforce Setup > Sharing Settings (organization-wide defaults) and the sharing rules per object; confirm any SOQL field for the defaults with describe before quoting it. HubSpot View access per object in each permission set ("All deals", "Their team's deals", "Their deals"). Attio object and list access levels (workspace-wide on Plus, per team and member on Pro and Enterprise).
- Script: `decisions.restricted_fields`, `access_checks[]` (field, role, readable, approved, confirmed, via), `record_access[]` (object, holds_restricted_records, default_access, readable_outside_approved, via, confirmed, decision_ref). An `access` coverage of unavailable makes the rule NOT CHECKED.
- Agent adds: HubSpot property access restrictions and Sensitive Data need Enterprise. Below Enterprise the only control is each permission set's View access for deals, so collect it and rate exposure from it: anyone who can view a deal can read every property on it, amounts included.
- Confirm: one access view for a restricted field and one for a role outside the approved list.

## ACC-06 Authentication and team structure support the permission model (B, never FAIL)

- WARN: two-factor or SSO is not enforced for all users; team-based visibility is used while active owners belong to no team.
- Agent adds: hand authentication findings to the security owner; the audit records the setting.

## INT-01 The integration inventory is complete and owned (A, automation gate)

- FAIL: an integration with write access is missing from `stack.md` and from the list the team believes is connected, or has no named owner.
- WARN: an entry lacks direction, objects, match key or fields written; a read-only integration has no owner.
- Evidence: HubSpot connected apps, legacy private apps, service keys, marketplace apps, webhook subscriptions, MCP connectors in use. Salesforce Connected Apps OAuth Usage (connected apps only, leaving out username-password flow apps), External Client App usage from the Connect REST API (hosted MCP connections included), installed packages, named credentials, `OauthToken`. Attio installed apps, API token names, `list-webhooks`.
- Script: `integrations[]` (owner, in_stack_md, believed_connected, direction, objects, match_key or match_keys, fields_written).

## INT-02 Each mapped field has one direction and defined blank and clear behaviour (A)

- FAIL: a two-way mapping, or a mapping where one app always wins, covers a critical field whose owner differs by system; blanks from an integration clear values another system owns.
- WARN: blank and clear semantics undefined; first-sync baseline not decided for newly paired records.
- Agent adds: in HubSpot data sync the default app wins discrepancies within the configured direction, and if the default app has no value nothing changes in the other app. For the HubSpot and Salesforce integration, a field's first sync takes the Salesforce value as the baseline, owner mapping is two-way, and the field rules (prefer Salesforce unless blank, always use Salesforce, two-way, don't sync) differ in how clears propagate.

## INT-03 Syncs are healthy (B)

- FAIL: an integration was suspended in the last 30 days; record errors above 5% of synced records.
- WARN: record errors above 1%; any error open for more than 7 days.
- Evidence: HubSpot Salesforce integration Sync Health and sync errors, data sync error pages; Salesforce middleware queues and failed jobs; Attio integration logs.

## INT-04 API capacity has headroom (B)

- FAIL: Salesforce daily API requests at or above 90% of the allocation in the rolling 24 hours.
- WARN: at or above 70%; HubSpot daily API usage at or above 80% of the account limit, which every app in the account shares.
- Evidence: Salesforce REST Limits resource `DailyApiRequests` (`sf org list limits`), which needs View Setup and Configuration and is accurate within about five minutes; System Overview API usage; API Total Usage event logs to attribute consumers (Salesforce lists this event, but its page would not open on 2026-11-06, so confirm its fields in the org). HubSpot account information API usage details and the usage guidelines page for the limit.
- Agent adds: Org Check itself warns at 70% and stops at 90% of the Salesforce allocation, so audits and backfills belong off-peak. The allocation is org-wide. A new integration user adds nothing unless its licence carries API calls, so read the total from the Limits resource after any licence change.

## INT-05 Credentials are current and owned (B)

- FAIL: a credential tied to a departed person still has write access: the person's own login or OAuth grant, or a token or private app they created or could read that has not been rotated since they left.
- WARN: HubSpot legacy private apps with no owner or migration plan (new ones cannot be created in existing accounts since 26 October 2026; existing ones keep working); OAuth apps or tokens unused for 90 days; the Salesforce DX local MCP server still configured (it reached end of life on 2 November 2026 and its repositories are archived).
- Script: `integrations[].credential_tied_to_departed_person` (defined in [inventory-format.md](inventory-format.md)), `last_used_at`, `legacy_private_app`, `migration_plan`, `settings.salesforce_dx_mcp_configured`.
- Evidence: HubSpot legacy private apps with creator and last use; Salesforce `SELECT AppName, LastUsedDate, UseCount FROM OauthToken`; Attio API token names and creation dates.

## INT-06 Known conflicting combinations are absent (A)

- FAIL: HubSpot Smart Transfer and the native Salesforce integration both active (they match records on different identifiers and create duplicates in both systems); owner rotation and an owner sync from Salesforce or an enrichment app both writing owners on the same records.
- WARN: a Salesforce inclusion segment treated as a security boundary; data sync filters assumed to exclude records after the first sync (they only limit the initial sync).

## INT-07 Enrichment and reverse ETL cannot overwrite owned fields (A)

- FAIL: an enrichment tool in overwrite mode may write a field another system or team owns (owner, segment, lifecycle, routing inputs); a reverse ETL sync clears values it does not own, or that other writers also write, when rows are missing.
- WARN: enrichment fills empty values only but keeps no provenance or observed-at field; a reverse ETL sync clears its own fields when rows are missing and has no guard against an empty model run; synced facts that automations act on have no freshness field.
- Agent adds: Attio upserts reconcile non-matching multi-value attributes to the supplied values, which can remove existing entries. HubSpot's Enrich record workflow action can overwrite existing data whatever the mapping's overwrite rule says, so read the action as well as the enrichment settings.

## REP-01 Headline metrics are defined (B, reporting gate)

- FAIL: a headline metric reported to leadership has no written counting unit or no timestamp.
- WARN: timezone, currency policy (for money metrics) or exclusions missing.
- Script: `reporting.metrics[]` from `metrics.md`.

## REP-02 Headline numbers reconcile to record lists (A, reporting gate)

- FAIL: a dashboard number differs from the count or sum over its record-ID list by more than 1% with no explained cause.
- WARN: reconciliation not run for one of the headline numbers (pipeline value, new customers, the top-of-funnel metric), including a reconciliation whose dashboard or record list value is missing, or whose record list value is zero.
- Never make a headline number reconcile by changing the report filter. Find the grain, population or currency difference.
- Evidence: HubSpot dashboard value against an export of record IDs and amounts with the same filters. Salesforce report value against a SOQL or report export of IDs; Smart Totaling can count a repeated parent amount once in the report while a flat export adds it several times.

## REP-03 Reports count at the right grain (A, reporting gate)

- FAIL: a headline report sums deal or opportunity amount after joining to contacts, products, activities or workspaces; a Salesforce report type "with" a child object drops parents the metric needs; unique counts run on names instead of IDs.
- WARN: the grain is not stated in the report title or description.
- Grain trap with numbers: three deals of 24,000, 10,000 and 10,000, where the first has two contacts. Summed after a contact join, the total is 68,000. The correct total is 44,000. `SUM(DISTINCT amount)` gives 34,000 and is also wrong, because two different deals share an amount. Deduplicate on the record ID, then sum.

## REP-04 One currency policy (A, reporting gate)

- FAIL: amounts in different currencies are summed without conversion; company currency changed after deals existed with no documented conversion.
- WARN: multi-currency is on with no documented exchange-rate policy.
- Agent adds: changing HubSpot company currency later does not convert existing deal amounts.

## REP-05 Pipeline hygiene supports forecasting (B)

- FAIL: more than 25% of open deals have a past close date; more than 30% of forecast-stage deals lack an amount; fewer than 50% of deals closed lost in the lookback window have a reason.
- WARN: more than 10%; more than 10%; fewer than 80%.
- crm-data-hygiene-audit L05 fails sooner (more than 10% of open deals over 30 days past close) because it hands over the record list to fix. A CRM can read WARN here and FAIL there; report each under its own rule ID.

## REP-06 Source attribution coverage (B)

- FAIL: more than 50% of contacts created in the last 90 days have an unknown or offline source. WARN: more than 30%; deals have no source snapshot taken at creation.
- Agent adds: imported contacts have no web history. Report that group separately rather than reassigning it.

## REP-07 Exclusions and funnel timestamps are consistent (C, never FAIL)

- WARN: no single documented rule excludes test and internal records in both the CRM and analytics; a lifecycle funnel built on a custom field without stage timestamps; the funnel re-entry rule undocumented.

## API capacity planning for INT-04

Plan any backfill or resync inside a budget, not against the full allocation:

```text
remaining in the rolling 24 hours        60,000 calls  (Limits resource, read just before)
protected reserve for essential syncs  - 25,000
other consumers expected in the window - 15,000
budget for this job                     = 20,000
job: 9,000 records x 3 calls per record = 27,000  -> over budget
```

Split the job across windows, reduce calls per record, or ask the vendor about a temporary increase. Bulk API requests still count toward total API consumption in Salesforce's current limits reference. Re-read the Limits resource between batches, because other consumers move the number.
