# Log retention is bounded by rotation config, IaC retention settings, or platform-managed logging

- **Pattern:** `ab-002640` (`soc2-readiness.data-lifecycle.log-retention-configured`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-07-03
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002640
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002640)

## Why it matters

Application logs quietly accumulate confidential data: IP addresses, emails, session identifiers, request payloads. SOC 2 C1.2 requires that confidential information is disposed of when no longer needed, and unbounded log retention is the most common quiet violation of it (bounded retention also evidences CC7.2, since a log pipeline someone actually configured is one someone actually monitors). Auditors ask "how long do you keep logs, and where is that enforced": the answer must point at a rotation config, an IaC retention setting, or a platform whose defaults bound it. AI-generated and vibe-coded apps typically fail this because tutorials show `new winston.transports.File(...)` and never the lifecycle: the transport ships, the retention bound never does, and the log directory grows until the disk fills or a subpoena arrives. GDPR's storage-limitation principle (Art. 5(1)(e)) applies the same pressure from the privacy side. The good news is that the dominant deploy targets (Vercel, Netlify, Heroku) manage log retention themselves, so most stdout-only apps pass with a note; the check exists to catch the self-managed file and ELK/Loki setups where nobody ever set a bound.

## Severity rationale

Low because unbounded log retention is a slow-accruing confidentiality and disk exposure rather than an immediate breach vector, and platform-managed deployments bound it by default; the mechanism is retained confidential data with no disposal path.

## Remediation

Put a retention bound wherever logs persist. For file transports, replace the plain file transport in your logger setup (e.g. `src/lib/logger.ts`) with a rotating one that deletes old files:

```ts
import DailyRotateFile from 'winston-daily-rotate-file';
logger.add(new DailyRotateFile({
  filename: 'logs/app-%DATE%.log',
  maxSize: '20m',
  maxFiles: '30d', // rotation without maxFiles rotates but never disposes
}));
```

For platform log groups, set the bound in IaC (`retention_in_days = 30` on an `aws_cloudwatch_log_group`, `retention_days` on a GCP logging bucket). For self-hosted Loki set `retention_period` under `limits_config`; for self-hosted Elasticsearch commit an ILM delete policy. If you deploy to Vercel, Netlify, or Heroku and log only to stdout, the platform bounds retention for you: nothing to add.

## Detection

- **ID:** `log-retention-configured`
- **Severity:** `low`
- **What to look for:** First map how the project logs and where logs persist. Logging libraries in `package.json`: `winston`, `pino`, `bunyan`, `log4js`, `morgan`, plus rotation add-ons `winston-daily-rotate-file`, `pino-roll`, `rotating-file-stream`; Python equivalents: `logging.handlers.RotatingFileHandler` / `TimedRotatingFileHandler`, `loguru` with `rotation=` / `retention=`; Go: `lumberjack` (`MaxAge`, `MaxBackups`). Self-managed log persistence signals: `new winston.transports.File(...)`, `pino.destination('...')` or `pino/file` targeting a `.log` path, `fs.createWriteStream` appending to `logs/`, `log4js` file appenders, a `logs/` dir in `.gitignore` paired with a file transport; self-hosted log stacks in `docker-compose*.yml` / `k8s/` (services named `elasticsearch`, `logstash`, `kibana`, `loki`, `promtail`, `graylog`, `fluentd` / `fluent-bit` with file outputs). Retention bounds to hunt for: `maxFiles` / `maxSize` on rotate transports, `logrotate.conf` or `/etc/logrotate.d/*` entries copied in a `Dockerfile`, Loki `retention_period` under `limits_config` (or table_manager retention), an Elasticsearch ILM policy file, IaC settings in `*.tf` / CDK / Pulumi (`retention_in_days` on `aws_cloudwatch_log_group`, CDK `logRetention`, GCP logging bucket `retention_days`, log sink TTL). Platform-managed leg: deployment config for a platform that bounds log retention itself (`vercel.json` or `.vercel/`, `netlify.toml`, `Procfile` for Heroku, `fly.toml`, `render.yaml`) combined with logging that goes only to stdout/stderr or a hosted log service, with no self-managed log files.
- **Pass criteria:** Any ONE of: (a) a file or rotating transport that is actually wired into the logger AND carries an explicit disposal bound (`maxFiles` on winston-daily-rotate-file or pino-roll, `backupCount` on RotatingFileHandler, `retention=` on loguru, `MaxAge`/`MaxBackups` on lumberjack, or a logrotate rule whose path matches the log output); (b) an IaC log retention setting (`retention_in_days`, `logRetention`, `retention_days`, sink TTL) present in committed infrastructure code; (c) platform-managed logging: a Vercel/Netlify/Heroku/Fly/Render deployment config exists and no self-managed log files are written anywhere, in which case PASS with a note naming the platform (platform defaults bound retention).
- **Fail criteria:** Self-managed log infrastructure exists with no retention bound anywhere: a file transport writing to disk with no `maxsize`/`maxFiles` and no logrotate rule or IaC retention covering it; a self-hosted ELK/Loki/Graylog stack in compose or k8s manifests with no `retention_period`, ILM policy, or curator config. Sneaky variants that still FAIL: `winston-daily-rotate-file` or `pino-roll` present in `package.json` but never instantiated while a plain unbounded file transport does the actual writing (installed is not wired); a DailyRotateFile wired WITHOUT `maxFiles` (rotation without retention rotates forever and disposes of nothing); a `maxFiles` line that is commented out; a `logrotate.conf` committed whose paths do not match where the app actually writes logs; a Loki config file present but the `retention_period` key absent or set to `0`. Do NOT fail a stdout-only app on a bounded platform, and do NOT fail for the retention window being "too long": any explicit bound passes; judging the number is out of scope.
- **Skip (N/A) when:** No logging at all and no deployment target: no logging library in the manifest, no log file writes, no log stack in compose, and no platform deployment config (a bare library package). Quote: `"No logging library, log file writes, or deployment config — bare library with no log retention surface"`.
- **Before evaluating, quote:** The logging setup evidence first (file path plus a 1-3 line excerpt of the transport/handler wiring, or the compose service names for a self-hosted stack). Then the retention evidence: the exact line carrying the bound (`maxFiles: '30d'`, `retention_in_days = 30`, `retention_period: 720h`, the logrotate rule) with its file path, OR the platform config path plus a statement that no self-managed log file writes were found after checking the enumerated transports. If failing, quote the unbounded transport line and state which bound locations were searched and empty.
- **Report even on pass:** `"Retention bound: <rotate-transport maxFiles | iac retention_in_days | logrotate rule | platform-managed (<platform>)> at <path>"`.
- **Detail on fail:** `"src/lib/logger.ts wires new winston.transports.File({ filename: 'logs/app.log' }) with no maxsize/maxFiles; winston-daily-rotate-file is in package.json but never instantiated, and no logrotate or IaC retention config exists — log retention is unbounded"` or `"docker-compose.yml runs loki + promtail but loki-config.yml has no retention_period under limits_config — self-hosted logs are kept indefinitely with no disposal path"`.
- **Remediation:** Add a disposal bound where logs persist: switch the file transport in `src/lib/logger.ts` to `winston-daily-rotate-file` with `maxSize: '20m', maxFiles: '30d'` (rotation without `maxFiles` never deletes), or set `retention_in_days = 30` on the `aws_cloudwatch_log_group` in Terraform, or `retention_period` under Loki's `limits_config`. Stdout-only on Vercel/Netlify/Heroku already passes. Deeper log-content and audit-trail coverage: `saas-logging`.

## External references

- soc2 C1.2
- iso-27001 A.8.15
- nist AU-11

Taxons: observability, privacy-consent

HTML version: https://auditbuffet.com/patterns/ab-002640
