# Audit Policy Configuration

## Overview

Audit Policy Configuration allows administrators to define precisely which entities, fields, and operation types are tracked by the audit system. Rather than auditing everything indiscriminately — which carries significant performance costs — policies let organizations target the data changes that matter most for compliance, security, and operational oversight. Each policy specifies a target entity, a single operation type (CREATE, UPDATE, or DELETE) to record, the subset of fields to capture, and how sensitive field values should be handled during capture. To audit multiple operation types on the same entity, administrators create separate policies — one per operation type.

Policies follow a lifecycle state machine (DRAFT → ACTIVE ↔ INACTIVE) that enables safe rollout and rollback of audit configurations. A policy in DRAFT can be reviewed and refined before it takes effect; activating a policy begins audit capture; deactivating a policy stops capture without losing the policy definition or any previously recorded audit entries. Policies can be scoped globally or to a specific company, supporting multi-company deployments where different legal entities have different compliance requirements.

A uniqueness constraint ensures that at most one ACTIVE policy exists for a given combination of (entityName, companyId, operationType). This eliminates ambiguity about which field set applies and avoids unintended field-set merges. To change the audited fields for an existing active configuration, use the atomic replacement operation (`replaceAuditPolicy`), which deactivates the current ACTIVE policy and activates the replacement DRAFT policy in a single transaction — guaranteeing no gap in audit coverage. Alternatively, an administrator can manually deactivate the current policy and then activate the replacement, but this two-step approach creates a brief window where no policy is active and events for that entity + operation combination will be silently discarded.

Policy scope must match the target entity's scope to eliminate precedence ambiguity. Company-bound entities (entities that always carry a companyId) can only have company-scoped policies — global policies (companyId = null) are rejected at creation time for company-bound entities. Conversely, global entities (entities that never carry a companyId, e.g., user accounts, currencies) can only have global policies — company-scoped policies are rejected at creation time for global entities. This scope-matching rule ensures that for any given operation, exactly zero or one ACTIVE policy can match. Entity scope (company-bound vs global) is declared at entity registration time and is immutable. Because entityName and companyId are immutable after creation, the audit module validates this scope-matching constraint at policy creation time only.

## Business Purpose

- Regulatory compliance demands that certain fields (e.g., financial amounts, approval statuses) are tracked, while auditing every field is unnecessary and expensive
- Security teams need visibility into sensitive data changes (e.g., user role assignments, payment details) without flooding the audit log with noise
- Sensitive data protection requires that field values containing personal data, credentials, or payment details are masked, hashed, or excluded from audit logs to prevent the audit trail from becoming a secondary store of sensitive information
- Performance optimization requires granular control — Dynamics 365, SAP, and other major ERPs explicitly warn that broad audit configurations degrade system throughput
- Multi-company organizations need different audit policies per legal entity to reflect varying local regulatory requirements
- Safe rollout of audit changes is critical — a misconfigured policy should be testable in DRAFT before it starts capturing production data
- Rollback capability allows administrators to deactivate a policy immediately if it causes performance issues or captures unintended data

## Process Flow

```mermaid
stateDiagram-v2
    [*] --> Draft: createAuditPolicy
    Draft --> Active: activateAuditPolicy
    Draft --> Active: replaceAuditPolicy (atomic swap:\ndeactivates current ACTIVE,\nactivates this DRAFT)
    Active --> Inactive: deactivateAuditPolicy
    Inactive --> Active: reactivateAuditPolicy
    Draft --> [*]: deleteAuditPolicy
```

The policy creation and configuration flow follows this pattern:

```mermaid
flowchart TD
    A[Create Audit Policy] --> B[Specify Target Entity]
    B --> C[Select Operation Type]
    C --> D[Add Field-Level Rules]
    D --> D2[Set Sensitivity Mode per Field]
    D2 --> E{All fields configured?}
    E -->|No| D
    E -->|Yes| F{Ready to activate?}
    F -->|No| G[Continue editing]
    G --> B
    F -->|Yes| H{Conflicting ACTIVE policy exists?}
    H -->|Yes| H2{Use atomic replacement?}
    H2 -->|Yes| H3[replaceAuditPolicy:\nDeactivate existing + Activate new\nin single transaction]
    H2 -->|No| H4[Reject: deactivate existing first]
    H3 --> J[Audit Capture Begins]
    H -->|No| I[Activate Policy]
    I --> J
```

When a data operation occurs, the system evaluates active policies to determine whether to record an audit entry. Because policy scope must match entity scope, the lookup is unambiguous — there is at most one matching ACTIVE policy:

```mermaid
flowchart TD
    A[Data Operation Occurs] --> B{Entity is company-bound?}
    B -->|Yes| C[Look up ACTIVE policy for entity + companyId + operation]
    B -->|No| D[Look up ACTIVE global policy for entity + operation]
    C --> E{Policy found?}
    D --> E
    E -->|No| F[No audit entry]
    E -->|Yes| G{Field-level rules defined?}
    G -->|No| H[Audit all eligible fields on entity]
    G -->|Yes| I[Audit only specified fields]
    H --> J[Apply sensitivity modes to values]
    I --> J
    J --> K[Record Audit Entry with processed values]
```

## Scenario Patterns

- **Compliance-Driven Field Tracking**: A financial services company creates two policies — one for UPDATE and one for DELETE — on the Journal Entry entity, each tracking only the amount, account, and approval status fields. This satisfies SOX requirements without capturing every minor metadata change
- **Security Monitoring**: An administrator creates three policies (one per operation type: CREATE, UPDATE, DELETE) on the User Role Assignment entity, each capturing every field. This provides a complete record of privilege changes for security review
- **Sensitive Data Protection**: A policy auditing the Employee entity marks the socialSecurityNumber field with sensitivity mode EXCLUDE (omitted from audit logs entirely) and the salary field with MASK (stored as a masked value like "***"). This ensures the audit trail records that these fields changed without exposing the raw values
- **Gradual Rollout**: A new audit policy for the Sales Order entity is created in DRAFT and reviewed by the compliance team. After validation, it is activated in a single company first, then additional company-scoped policies are created and activated for other subsidiaries
- **Performance Remediation**: An overly broad policy auditing all fields on a high-volume entity is deactivated immediately when monitoring reveals degraded throughput. A replacement policy targeting only critical fields is created and activated
- **Multi-Company Differentiation**: A global organization creates separate policies for its EU and US subsidiaries — the EU policy audits personal data fields to comply with GDPR (with appropriate masking), while the US policy focuses on financial fields for SOX compliance
- **Atomic Policy Replacement**: An administrator needs to add new fields to an active policy. They create a new DRAFT policy with the updated field set and use `replaceAuditPolicy` to atomically deactivate the existing ACTIVE policy and activate the replacement in a single transaction — ensuring no gap in audit coverage
- **Manual Policy Replacement (with gap)**: An administrator deactivates the existing ACTIVE policy and then activates a replacement DRAFT policy in two separate steps. During the window between deactivation and activation, events for that entity + operation are silently discarded. This approach is acceptable when a brief audit gap is tolerable
- **Policy Cleanup**: A DRAFT policy that was created for evaluation but never needed is deleted without affecting any audit data

## Test Cases

- Audit policy lifecycle follows DRAFT → ACTIVE ↔ INACTIVE state machine
- Policies can only be created in DRAFT status
- Only DRAFT policies can be deleted; ACTIVE and INACTIVE policies cannot
- Target entity name is required and must be non-empty
- Exactly one operation type (CREATE, UPDATE, or DELETE) must be specified per policy
- Operation types are limited to the set CREATE, UPDATE, DELETE — invalid values are rejected
- A policy without field-level rules audits all eligible fields on the target entity (as defined by the entity's auditableFields registration)
- A policy with field-level rules audits only the specified fields (which must be a subset of the entity's auditableFields)
- Field names in field-level rules must be non-empty strings
- Duplicate field names within the same policy are rejected
- Activating a policy without a target entity is rejected
- A policy must have exactly one operation type — policies without an operation type cannot be activated
- Only ACTIVE policies trigger audit capture; DRAFT and INACTIVE policies do not
- Deactivating a policy stops new audit capture but does not delete previously recorded audit entries
- Reactivating an INACTIVE policy resumes audit capture
- Company-scoped policies apply only to operations within that company
- A global policy (no company scope) applies to operations on global entities
- At most one ACTIVE policy can exist for a given (entityName, companyId, operationType) combination — activating a conflicting policy is rejected
- Activating a policy that would conflict with an existing ACTIVE policy returns an error instructing the administrator to deactivate the existing policy first
- Creating a global policy (companyId = null) for a company-bound entity is rejected
- Creating a company-scoped policy for a global entity is rejected
- Entity scope (company-bound vs global) is validated at policy creation time
- For any given entity + operation, at most one ACTIVE policy can match because policy scope must align with entity scope
- Deleting an ACTIVE or INACTIVE policy returns an error
- Each field-level rule supports a sensitivity mode: CAPTURE (default, store raw value), MASK (store masked representation), HASH (store one-way hash), or EXCLUDE (omit value entirely)
- When sensitivity mode is MASK, the stored value replaces all but the last 4 characters with asterisks (or stores "***" if the value is 4 characters or fewer)
- When sensitivity mode is HASH, the stored value is a one-way hash of the original value
- When sensitivity mode is EXCLUDE, the ChangeDetail records that the field changed but stores null for both oldValue and newValue
- Fields without an explicit sensitivity mode default to CAPTURE (raw value stored)
- Sensitivity modes are enforced at write time — once an audit entry is persisted, the original raw value cannot be recovered from MASK, HASH, or EXCLUDE entries
- `replaceAuditPolicy` atomically deactivates the current ACTIVE policy and activates the replacement DRAFT policy in a single transaction
- `replaceAuditPolicy` requires the replacement policy to be in DRAFT status — non-DRAFT replacements are rejected
- `replaceAuditPolicy` requires an existing ACTIVE policy for the same (entityName, companyId, operationType) — if none exists, the operation is rejected (use `activateAuditPolicy` instead)
- After `replaceAuditPolicy`, the previously active policy is in INACTIVE status and the replacement is in ACTIVE status
- During manual (non-atomic) replacement, events arriving between deactivation and activation are silently discarded
- Creating, updating, activating, deactivating, reactivating, replacing, and deleting audit policies requires the `manageAuditPolicies` permission at the appropriate scope
- A caller with company-scoped `manageAuditPolicies` can only manage policies for their assigned companies
- A caller with global-scoped `manageAuditPolicies` can only manage global policies
- A caller without `manageAuditPolicies` permission receives an authorization error when attempting policy operations

## Reference Links

- [Dynamics 365 Configure Entity and Attribute Auditing](https://learn.microsoft.com/en-us/power-platform/admin/manage-dataverse-auditing)
- [SAP Change Document Objects](https://help.sap.com/docs/ABAP_PLATFORM_NEW/b5670aaaa2364a29935f40b16499972d/4873464e4cf611d3a6510000e835363f.html)
- [Odoo Tracking Field Changes](https://www.odoo.com/documentation/19.0/developer/reference/backend/orm.html)
- [AWS CloudTrail Trail Configuration](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-concepts.html)
- [Microsoft Dataverse Auditing](https://learn.microsoft.com/en-us/power-platform/admin/manage-dataverse-auditing)
