# Evidence sources: getting every input read-only

Vendor paths and limits here were checked on 2026-11-06 unless a line says otherwise. Menus move, so confirm each path in the live docs before quoting it to the user.

Prefer exports over live connections. An export needs no credential in the conversation, can be re-read, and its date becomes the evidence date. Use a vendor MCP server only in the read-only mode listed in [handoffs.md](handoffs.md).

## What each channel sees and misses

- Spend (accounts payable, corporate cards, expenses): catches purchases outside procurement and second payers. Misses free apps with data access, fees netted at source, and whether the tool does any work.
- Contracts (executed order forms and amendments): show commitments, renewal dates, notice terms and reduction rules. Miss card purchases and whether anyone uses the tool.
- Identity (SSO sign-ins and OAuth grants): show who signs in and which apps were connected to company accounts, including free ones. Miss tools without SSO, service accounts and background jobs.
- Admin consoles and logs: show jobs, API keys, workflows, meters and real usage units. Miss the commercial exposure.
- Browser page load: shows tags and pixels that send data. Misses server-side and after-login tools. A detected script proves neither use nor a contract.
- Operators: the only source for spreadsheets, CSV hops and the quarterly user who cannot lose access.

A tool seen in one channel only is a question, not a finding. stack_inventory.py lists those tools so someone checks the other channels.

## Spend

- Ask finance for 12 months of lines from the software, SaaS, subscription, marketing and data GL accounts or cost centres, plus all card and expense lines, where purchases outside procurement hide. Each line needs date, vendor, amount, currency and payment source. Credits stay as negative amounts.
- Before reading INV01, add any non-martech vendor that still appears (rent, payroll, cloud hosting, legal) as an out_of_scope row with a reason in `evidence_ref`.
- Replace card numbers and cardholder names with a payment method name ("Card A", "Expense claim") before the file is shared.
- Headers the script accepts, case-insensitively: date (transaction date, posted date, payment date, invoice date, booking date); vendor (merchant, payee, supplier, name); amount (total, debit); currency (ccy, currency code); payment source (payment method, card name, account, source). Comma, semicolon or tab separators work, and so do comma decimals such as 3.200,00. Pass `--date-format %d/%m/%Y` for day-first dates or `%m/%d/%Y` for month-first US exports; a date the script cannot read stops the run with its row and column.
- A missing currency column becomes UNSPECIFIED and is never added to another currency.
- Set `spend_type` on each register row: media (ad spend) and services (agency retainers) are reported apart; netted means fees deducted at source, such as payment processor fees, which never appear as charges.
- Finance MCP servers, read-only use only:
  - Ramp's hosted MCP server (`mcp.ramp.com/mcp`): choose Read-only on the consent screen, because Full access is the default, and connect a user whose Ramp role fits the audit. Responses cap at 100 rows per query (agents.ramp.com, checked 2026-11-06), so prefer a transaction export for 12 months of lines. The open-source ramp_mcp repository was archived on 17 July 2026.
  - The QuickBooks Online MCP server (Apache-2.0) has create, read, update and delete tools across 29 entities and 11 reports. Set `QUICKBOOKS_DISABLE_WRITE`, `QUICKBOOKS_DISABLE_UPDATE` and `QUICKBOOKS_DISABLE_DELETE` to true so only get and search tools register; the OAuth grant itself can still write, so prefer a report export.
  - The Xero MCP server (MIT) includes invoice creation. Prefer report exports, or allow only read tools.
  - Zylo's MCP server can act as well as answer (create contracts, update licence counts, reassign owners). Its permissions mirror the API key, so use a key limited to reads (Zylo post dated 2026-07-14).

## Contracts

- Read executed order forms and amendments, not the vendor's public terms. Public terms are defaults that the order can override.
- Capture per order: legal entity, product, term end, notice days, notice mechanism (account toggle or written notice), required recipient as the order describes it, auto-renew, pricing basis, reduction rule, discount end, quantities.
- Who may send notice, and how, is the contract owner's call. Record it; the audit never sends it.

## Identity provider and OAuth exports

- Okta: Admin Console, Reports, Reports, Application Usage, with 90 days of events. With Application set to All, it is a CSV download only, the columns are Person, Login, Application username, Instance name, Login Date and Login Date_ISO8601, the filter is by group only, and there is no Logins column. A single-app report adds a Logins count. stack_inventory.py takes the latest date per person and app.
- Microsoft Entra ID: sign-in and audit logs are kept 7 days on Free and 30 days on P1 and P2 (Microsoft Learn page updated 2026-03-25). Upgrading the licence does not bring back older data; longer history needs archiving to storage. A 7 or 30 day source cannot prove 45 days of dormancy, so use the tool's own last login (USE03). Verify enterprise application report paths at run time.
  - stack_inventory.py reads Entra, or any sign-in source other than the Okta report, as a generic file with three columns: `app` (the application name), `user_ref` (the user ID or UPN) and `last_login` (the latest successful sign-in date per user and application; filter the download to successful sign-ins first). Pass `--sso-retention-days 7` or `30`; the script stops without it. A filled date it cannot read also stops the run; a blank one means no login. Verify the download's column names at run time.
  - Microsoft 365 app grants: `--oauth` reads only the Google file, so read these grants and record INV02 and ACC06 by hand ([governance-access-ai.md](governance-access-ai.md)). In the Entra admin center: Entra ID, Enterprise apps, All applications, the app, Permissions, then the Admin consent and User consent tabs (page updated 2026-02-19). That page asks for at least a Cloud Application Administrator, a role that can also revoke, so prefer the read route: Microsoft Graph as a Directory Readers user, `GET /servicePrincipals/{id}/oauth2PermissionGrants` for delegated grants (Directory.Read.All) and `GET /servicePrincipals/{id}/appRoleAssignments` for application permissions (Application.Read.All). Ask the IT admin for the export, and verify a tenant-wide export path at run time.
- Google Workspace: Security, Access and data control, API controls, Manage Third-Party App Access, Bulk update list, Download Accessed Apps. Super administrator only. Columns: App Name, Type, Id, Verification Status, Users, Requested Services, Access, Requested Services with Scopes, Org Unit (page updated 2026-10-26).
  - Download only. The bulk upload on the same screen replaces Type, Id, Access and Org Unit settings in the console. The audit never uploads an edited copy.
  - Token audit events come from the Admin SDK Reports API; verify its retention at run time.

## Tool-native user exports

- HubSpot: Users & Teams, Actions, Export all users (Super Admins). The export includes each user's role, permissions and Last login date (page updated 2026-09-21).
- HubSpot Connected Apps (Settings, Integrations, Connected Apps) shows the installer, installed date, app owner and last activity date, meaning data last sent and received (page updated 2026-10-06).
  - Only some HubSpot-built apps show a connection status. User-level and multi-account apps show no last activity date.
  - The insights tab shows Activities, Record Insights, Automation Insights and API Call Usage: use it before calling a quiet integration shelfware.
- HubSpot remote MCP server: every action follows the connected user's permissions, and there is no read-only switch. Confirm the user and organisation tools in the live tool list before relying on their names.
- Salesforce (read in a browser on 2026-11-06):
  - User records with LastLoginDate (the last successful login; it refreshes only when 60 seconds have passed since the previous one) and profile, through a report or the `sobject-reads` hosted MCP server.
  - Licence counts sit on Company Information, in the User Licenses related list. Changes can take up to 24 hours to show.
  - Connected Apps OAuth Usage covers connected apps only: current OAuth connections, and first use, latest use and total uses per user. It leaves out apps on the username-password flow. Reading it needs Customize Application, View Setup and Configuration, and Manage Users, so ask an administrator for it.
  - External Client App usage, which includes hosted MCP connections, has no usage page and comes from the Connect REST API. New connected apps have been restricted since Spring '26, so read both lists before calling a Salesforce integration quiet or an AI connection absent.
  - Setup Audit Trail holds configuration changes: the Setup page shows the 20 most recent, the download covers the past 180 days, and older records are deleted.
- GA4 and Tag Manager: access lists show roles but no login dates, so dormancy is NOT CHECKED for them. Read them in each product's access management screen (verify the path at run time). Google's GA4 MCP server reads with `get_account_summaries`, `get_property_details`, `list_google_ads_links` and `run_report`; never pass `--allow-write`.
- Snowflake: `SNOWFLAKE.ACCOUNT_USAGE.USERS` gives LAST_SUCCESS_LOGIN (NULL when older than the one-year login history), DISABLED, TYPE (service users) and OWNER; LOGIN_HISTORY covers 365 days; both views can lag up to 2 hours and need imported privileges on the SNOWFLAKE database or a SNOWFLAKE database role, granted by an administrator. Query them with a SELECT-only role on an extra-small warehouse with a statement timeout; through the managed MCP server that role must be the connecting user's DEFAULT_ROLE, with `read_only` true and `OAUTH_USE_SECONDARY_ROLES` at NONE.
- BigQuery: `INFORMATION_SCHEMA.JOBS` covers 180 days with user_email, principal_subject and total_bytes_billed. It needs a region qualifier and `bigquery.jobs.listAll` plus `bigquery.jobs.create` on the project; BigQuery Resource Viewer holds both, and BigQuery User also creates datasets, so prefer the first. On the BigQuery remote MCP server use `execute_sql_readonly`, and deny `execute_sql` and `cancel_job` in the client and with an IAM deny policy. The tool has no maximum bytes billed, so agree a budget, dry-run first, then set `jobTimeoutMs` and a label. INFORMATION_SCHEMA queries bill at least 10 MB each on demand and are never cached.

## MFA and identity provider status

- MFA (seat column `mfa`, check ACC04): read each critical tool's security settings for whether it requires two-factor authentication or SSO and whether password sign-in stays open beside SSO, and the user export where it shows MFA per user. An identity provider policy counts only when the account cannot sign in another way.
- Identity provider status (seat column `idp_status`, check ACC05): export the identity provider's user list with each user's status (Okta people, Entra users or Google Workspace users; verify the path at run time) and join it to each tool export by email inside the audit folder. External and service accounts are not_in_idp or n/a.

## Pipelines, iPaaS and warehouse jobs

- List every connection, sync and workflow with its creator and the credential it runs on. The creator and the runtime identity differ more often than the UI suggests.
- Fivetran's MCP server: `FIVETRAN_SCOPE=read` plus the README's recommended denylist for key operations.
- Reverse ETL and iPaaS tools: export the sync or workflow list with owner, schedule, last run and error counts. Verify each product's export path at run time.
- Ask operators for every manual CSV upload: who runs it, how often, and from which file.

## Browser evidence

- Use a fresh profile with no extensions. Load the key pages in three consent states: no choice, rejected, accepted. The no-choice load needs no click; rejecting or accepting on a live banner needs the owner's approval for that test. Keep the HAR or network log local.
- Public detection lookups such as Wappalyzer are seeds for the inventory, not proof of use or of a contract.

## Normalising user exports into seats.csv

Use the columns of [../assets/seats-template.csv](../assets/seats-template.csv), one row per account per tool.

- `is_admin`: yes for the tool's top-level admin role. Examples: HubSpot Super Admin; Salesforce System Administrator; GTM account Administrator or container Publish; GA4 Administrator; Snowflake ACCOUNTADMIN or SECURITYADMIN; the Admin role in most other tools.
- `paid_seat`: yes when the account uses a paid seat. Viewer, free and core seats are no.
- `status`: active, disabled or invited. Disabled rows are ignored.
- `last_login`: YYYY-MM-DD, blank or never when the source shows no login, unknown when nobody can tell. Pass `--date-format` for another format; a date the script cannot read stops the run with its row and column.
- `source` and `source_retention_days`: name the export and how many days of logins it keeps. Leave the number blank for tool-native history; write n/a when the source has no login data at all, which excludes those rows from dormancy; write unknown when nobody can tell, which counts rows without a recent login as unknown activity (USE03).
- `requires_access`: yes for people whose role needs periodic access, such as a quarterly board reporter. They are not counted as reclaimable.
- `mfa`: enforced, optional, unknown, or n/a for accounts with no interactive sign-in.
- `idp_status`: active, deactivated, not_in_idp, or n/a.
- `email` is used only for the domain (company or external) and for generic local parts, as in marketing@example.com. The script never prints it.

## Privacy

- Exports stay on the user's machine, in the audit folder. The scripts print counts, domains and row numbers, never email addresses.
- Ask for internal IDs rather than customer records. When only emails exist, key_match.py writes exceptions as SHA-256 hashes.
- Remind the user to delete the raw exports when the audit closes, or to follow the company's retention rule. The agent deletes nothing itself.
