Archive Retention Enforcement¶
Version: v1.2
Plan: PLAN-1611
Status: Production-ready
Overview¶
Archive retention enforcement is a hard compliance gate built into Cesivi's archive subsystem. Every imported item carries a retention record that specifies how long the item must be preserved. While an item is within its retention window, all delete and modify operations are unconditionally blocked — no administrator, service account, or API key can override this gate.
This document covers:
- Policy configuration
- Anchor semantics
- Hard enforcement guarantee
- Extension workflow
- Retention dashboard tour
- REST API reference
- Troubleshooting
- Compliance mapping
Policy Configuration¶
Retention policy is set per archive site and applies to all items imported into that site. The policy determines:
| Field | Default | Description |
|---|---|---|
DefaultWindowDays |
2555 (7 years) | Retention window in calendar days from the anchor date |
Anchor |
ImportDate |
Which date is used as the start of the retention window |
AllowExtension |
true |
Whether operators may extend the window via the admin API |
PolicyVersion |
1 | Schema version (increment on policy changes) |
Setting the policy via REST¶
PATCH /_api/archive/retention/policy?siteId={archiveSiteId}
Content-Type: application/json
{
"DefaultWindowDays": 3650,
"Anchor": "ItemCreated",
"AllowExtension": true,
"PolicyVersion": 1
}
Important: Policy changes apply only to new imports. Existing retention windows are never retroactively shortened. A policy change cannot reduce an already-assigned retention window — the longer of (existing, new) always wins.
Anchor Semantics¶
The retention window starts from the anchor date. Four anchor modes are available:
| Mode | Description |
|---|---|
ImportDate |
The date/time the item was imported by the MigrationTool (default; always available) |
ItemCreated |
The item's Created date from the source SharePoint farm |
ItemModified |
The item's Modified date from the source SharePoint farm |
CustomField |
Value from a named DateTime/DateTimeOffset field (specified in CustomFieldName) |
If Anchor = CustomField and the field is absent or unparseable, Cesivi falls back to ImportDate and logs a WARN-level message, recording the fallback in the WORM audit event (RetentionAnchorAssigned.FallbackUsed = true).
Example: A legal archive with DefaultWindowDays=3650 and Anchor=ItemCreated means every document is retained for 10 years from its original creation date in the source SharePoint farm.
Hard Enforcement Guarantee¶
There is no admin override. Retention is a hard compliance gate.
- No request header bypasses it.
- No role or service account bypasses it.
- The only legitimate path to removing an item before its window closes is Legal Hold release (PLAN-A-7, future capability).
What gets blocked¶
| Operation | HTTP Surface | CSOM Method | SOAP Method |
|---|---|---|---|
| Delete list item | DELETE /_api/web/lists/.../items(N) |
ListItem.DeleteObject() |
Lists.UpdateListItems Cmd=Delete |
| Recycle list item | POST /_api/web/lists/.../items(N)/recycle |
ListItem.Recycle() |
— |
| Delete file | DELETE /_api/web/getfilebyserverrelativeurl(...) |
File.DeleteObject() |
— |
| Recycle file | — | File.Recycle() |
— |
| Delete folder | — | Folder.DeleteObject() |
— |
| Recycle folder | — | Folder.Recycle() |
— |
| Delete attachment | DELETE /_api/web/lists/.../items(N)/AttachmentFiles('...') |
— | — |
| Modify field | PATCH /_api/web/lists/.../items(N) |
(update item) | Lists.UpdateListItems Cmd=Update |
HTTP responses on block¶
| Surface | Status Code | Body |
|---|---|---|
| REST | 409 Conflict |
{ "error": { "code": "-2147024891", "message": "Item is within its retention window until …" } } |
| CSOM | 500 with CesiviRetentionException in context |
CSOM error payload |
| SOAP | SOAP fault 0x81020015 |
SOAP error element with block reason |
Extension Workflow¶
Operators with site-admin privileges may extend a retention window — but never shorten it.
Via REST API¶
POST /_api/archive/retention/extend
Content-Type: application/json
{
"SiteId": "00000000-0000-0000-0000-000000000000",
"ListId": "00000000-0000-0000-0000-000000000001",
"ItemId": 42,
"NewUntilUtc": "2034-01-01T00:00:00Z",
"Reason": "Litigation hold extended by Legal"
}
Response (200 OK):
{ "extended": true, "newUntilUtc": "2034-01-01T00:00:00Z", "oldUntilUtc": "2030-01-01T00:00:00Z" }
Rejection (400 Bad Request — shortening attempt):
{ "error": "New retention date (…) must be later than the current window end (…). Retention windows cannot be shortened." }
Every extension emits a RetentionWindowExtended event to the WORM audit log and broadcasts a RetentionExtended SignalR message to the live-tail dashboard.
Retention Dashboard Tour¶
Navigate to Cesivi Administration → Archive → Retention.
Header KPIs (top row)¶
| Card | Meaning |
|---|---|
| Total Items | Number of items with any retention record in the site |
| In Retention | Items still within their window (cannot be deleted/modified) |
| Expiring < 30 days | Items whose window closes within 30 calendar days (flag for review) |
| Blocked (24 h) | Blocked delete/modify attempts in the past 24 hours |
Per-site cards¶
One card per registered archive farm. Stats mirror the KPIs but scoped to that farm.
Blocked attempts table (live-tail)¶
The table shows the most recent blocked attempts (up to 100 rows). New rows arrive via SignalR in real time — within ~1 second of the REST rejection. Each row shows:
- Time — local time of the block
- Action — Delete, Recycle, ModifyField, etc.
- Item — canonical item key (last path segment shown; hover for full key)
- Principal — the caller's login name
- Surface — REST, CSOM, or SOAP
- Until — retention window end date (or "list-level" for file/folder operations)
REST API Reference¶
All endpoints require site-administrator privilege.
GET /_api/archive/retention/status?siteId=…¶
Returns per-site retention statistics.
Response:
{
"SiteId": "...",
"TotalItems": 12500,
"ItemsInRetention": 11200,
"ExpiringIn30Days": 47,
"ExpiringIn90Days": 213,
"ExpiringIn365Days": 1840,
"BlockedAttemptsLast24h": 3,
"AsOf": "2026-05-27T10:00:00Z"
}
GET /_api/archive/retention/items?siteId=…&untilBefore=…&untilAfter=…&cursor=…&top=…¶
Paged enumeration of retention records. Cursor is the ItemKey of the last returned record.
GET /_api/archive/retention/blocked-attempts?siteId=…&since=…¶
Blocked-attempt forensic feed. since defaults to 7 days ago.
POST /_api/archive/retention/extend¶
Extends a retention window. Body: { SiteId, ListId, ItemId, NewUntilUtc, Reason }. Rejects shortening with HTTP 400.
GET /_api/archive/retention/policy?siteId=…¶
Returns the current per-site retention policy.
PATCH /_api/archive/retention/policy?siteId=…¶
Updates the per-site default retention policy for new imports. Applies to future imports only.
Troubleshooting¶
Items unexpectedly blocked¶
- Check the blocked-attempts table in the dashboard — confirm the
ItemKeyandUntildate. - Call
GET /_api/archive/retention/items?siteId=…to see the retention record for the item. - If the window was assigned incorrectly (wrong anchor date), the operator can call
POST /_api/archive/retention/extendto set the correct window end date. The window cannot be shortened retroactively.
Items not blocked when expected¶
- Verify the item has a retention record:
GET /_api/archive/retention/items?siteId=…&listId=… - If no record exists, the ArchiveImporter may not have captured retention at import time. Run the backfill command:
cesivi migrate retention-backfill --site <archiveSiteId> - Check that
Cesivi:Retention:UseInMemoryis not set totruein production — InMemory store does not persist between restarts.
Blocked attempts not appearing in dashboard¶
- Verify SignalR is connected (green dot in top-right of the dashboard).
- Check that
ASPNETCORE_ENVIRONMENTis notTesting— some test configurations disable SignalR. - The dashboard only shows the last 100 rows in the browser; older records are in the store and accessible via
GET /_api/archive/retention/blocked-attempts.
High CPU from retention gate¶
The gate performs an in-memory lookup (O(1) for per-item checks; O(N) for list-level file/folder checks). If the archive site has millions of records and list-level operations are frequent, consider:
- Enabling FileSystemArchiveRetentionStore (default) rather than InMemory for production
- Pre-computing per-list indexes (planned for v1.3)
Compliance Mapping¶
The retention enforcement gate is designed to support the following regulatory requirements:
| Regulation | Requirement | How Cesivi Satisfies It |
|---|---|---|
| HIPAA § 164.530(j) | Retain PHI records for 6 years | Set DefaultWindowDays=2190 (6 years) per site |
| GDPR Art. 5(1)(e) | Erase personal data when no longer needed; storage limitation | Retention window defines the mandatory hold period; after expiry, items can be deleted |
| SOX § 802 | Retain financial records for 7 years | Set DefaultWindowDays=2555 (7 years, default) |
| SEC Rule 17a-4 | WORM-compliant storage with audit trail | Blocked-attempt log in FileSystem JSONL + WORM audit event; no in-place overwrites |
| ISO 27001 A.18.1.3 | Protection of records | Hard gate prevents accidental deletion; blocked attempts are audited to WORM log |
Disclaimer: Cesivi provides the technical enforcement mechanism. Consult your legal and compliance team to verify that your specific policy parameters (window length, anchor mode) satisfy applicable regulatory requirements in your jurisdiction.
See Also¶
- ARCHIVE_IMPORTER.md — how retention capture works at import time
- ARCHIVE_INTEGRITY.md — hash verification for imported items
- ARCHIVE_AUDIT.md — WORM audit log for all archive events
See also: Archive Admin Bundle — ControlCenter Quick Tour
See also: Archive Tools Operator Guide
See also: Tutorial G — SharePoint On-Premises Retirement Archive
See also: Cesivi Archive Variant A — Whitepaper
See also: Compliance Cookbook — HIPAA/GDPR/SOX/FRCP
See also: Archive Cluster Deployment Guide