Zum Inhalt springen

Open source / Warden / retention-warden

retention-warden

Lint a declarative data-retention policy (YAML or JSON): missing or invalid periods, indefinite retention, conflicting rules, data categories held in systems with no rule, unknown categories/systems, missing legal basis, expiry action, trigger and owner, over-long periods, overdue reviews. SARIF, GitHub Action. Zero deps.

GitHub
# clone from GitHub
git clone https://github.com/MrLouisWright/retention-warden.git

README

retention-warden

npm version CI Node License: MIT

Lint your data-retention policy as code: every data category gets a period, a legal basis and an action at expiry.

GDPR's storage-limitation principle, the UK GDPR and most security questionnaires ask the same thing: how long do you keep each kind of personal data, why, and what happens afterwards? The answer usually lives in a spreadsheet that drifts away from the systems it describes. retention-warden reads a small, declarative retention.yml (categories, systems and rules), checks it is complete and consistent, and fails CI when someone adds a new data category without a rule or sets an over-long or indefinite period.

CLI, library and GitHub Action. Node.js 18+, zero runtime dependencies, runs offline. By Louis Wright.

Features

  • Coverage: every category held in a system needs a rule (category-wide or system-specific); undeclared categories and systems are reported; unused categories are noted.
  • Periods: ISO 8601 (P30D, P6M, P2Y) or plain words (90 days, 6 years); invalid periods, missing periods and indefinite retention of personal data (without justification) are high severity.
  • Limits: defaults.maxPeriod and defaults.specialMaxPeriod (for special: true categories such as health data).
  • Consistency: conflicting rules for the same category and system, and exact duplicates.
  • Accountability: legal basis, action at expiry (delete, anonymise, archive, review, ...), trigger (when the clock starts), owner, and a reviewed date checked against reviewEveryMonths (default 12).
  • Precise locations: findings carry the line in your YAML file, so SARIF annotations land on the right rule.

Scope

retention-warden checks the policy document, not your databases: it does not discover data or delete anything. Pair it with your data inventory. It is not legal advice; periods and bases must come from your own legal review.

Rules

RuleDefault severityWhat it means
retention-parse-errorhighPolicy file is not valid YAML/JSON or has the wrong shape
retention-category-unmappedhighA data category held in a system has no retention rule
retention-period-missinghighRule has no retention period
retention-period-invalidhighRetention period cannot be parsed (use ISO 8601 like P2Y, or "30 days", "6 months")
retention-conflicthighTwo rules give the same category (and system) different periods or actions
retention-indefinitehighPersonal data is kept indefinitely without a documented justification
retention-category-unknownmediumRule or system refers to a category that is not declared
retention-system-unknownmediumRule refers to a system that is not declared
retention-basis-missingmediumRule has no legal basis or justification for the period
retention-action-missingmediumRule does not say what happens at expiry (delete, anonymise, archive, review)
retention-exceeds-maxmediumPeriod is longer than the policy maximum (maxPeriod / specialMaxPeriod)
retention-trigger-missinglowRule does not say when the period starts (collection, last activity, contract end, ...)
retention-owner-missinglowRule or system has no accountable owner
retention-review-overduelowPolicy review date is missing or older than reviewEveryMonths
retention-duplicatelowIdentical rule declared twice
retention-category-unusedinfoDeclared category is not held in any system and has no rule
allowlist-expiredmediumAn allowlist entry is past its expires date
allowlist-unusedinfoAn allowlist entry matched nothing

Install and run

npx retention-warden
npx retention-warden policies/retention.yml --fail-on medium
npx retention-warden . --max-period P7Y --special-max-period P1Y
npx retention-warden --format sarif > retention.sarif

Sample output

retention-warden — test/fixtures/bad/retention.yml
policy test/fixtures/bad/retention.yml

HIGH     test/fixtures/bad/retention.yml:12 "chat" is held in postgres but no rule covers it [retention-category-unmapped]
HIGH     test/fixtures/bad/retention.yml:21 Rules #1 and #2 disagree for account: P2Y/delete vs P10Y/delete [retention-conflict]
HIGH     test/fixtures/bad/retention.yml:32 marketing is kept indefinitely [retention-indefinite]
HIGH     test/fixtures/bad/retention.yml:36 Period "soon" for invoices in s3 is not understood [retention-period-invalid]
MEDIUM   test/fixtures/bad/retention.yml:13 System "postgres" holds undeclared category "marketing" [retention-category-unknown]
MEDIUM   test/fixtures/bad/retention.yml:22 account kept 10y, over maxPeriod 5y [retention-exceeds-max]

Options

retention-warden 1.0.0 — lint a declarative data-retention policy

Usage: retention-warden [policy.yml|dir] [options]   (default: retention.yml in the current directory)

Policy
      --max-period <p>         Longest allowed period, e.g. P7Y (overrides defaults.maxPeriod)
      --special-max-period <p> Longest period for special-category data, e.g. P1Y
      --review-every-months <n>  Review interval (default 12)

Common
  -c, --config <file>     JSON config (default: .retention-warden.json or "retentionWarden" in package.json)
  -f, --format <fmt>      text (default) | json | sarif | markdown
      --json <file>       Also write a JSON report
      --sarif <file>      Also write SARIF 2.1.0
      --markdown <file>   Also write Markdown (e.g. for a job summary)
      --fail-on <sev>     critical | high | medium | low | info | none (default: high)
      --list-rules        Print rules and default severities
      --no-fixes          Hide fix suggestions in text output
      --no-color          Disable colours
  -v, --version / -h, --help

Exit codes: 0 = passed, 1 = findings at or above --fail-on, 2 = usage, config or read error.

Configuration and allowlist

Options can live in .retention-warden.json (or a "retentionWarden" key in package.json, or --config <file>):

{
  "failOn": "medium",
  "severity": {
    "retention-parse-error": "low"
  },
  "allowlist": [
    {
      "rule": "retention-indefinite",
      "subject": "audit-log",
      "reason": "Statutory audit trail, reviewed by DPO",
      "expires": "2027-03-31"
    }
  ],
  "maxPeriod": "P7Y",
  "specialMaxPeriod": "P1Y",
  "reviewEveryMonths": 6
}
  • severity changes a rule's severity, or turns it "off".
  • allowlist entries suppress matching findings. Each needs a reason and matches on any combination of rule, file, subject (globs with *) and match (a substring of the message). Add expires (YYYY-MM-DD) so the exception is time-boxed: once it lapses the finding comes back along with an allowlist-expired finding (medium). Entries that no longer match anything are reported as allowlist-unused (info).

The config file is looked up in the target directory first, then in the current directory, so .retention-warden.json can live at your repository root. Other keys: maxPeriod, specialMaxPeriod, reviewEveryMonths, requireSystemOwner and failOn.

Output and exit codes

  • --format text (default), json, sarif (SARIF 2.1.0 for GitHub code scanning) or markdown (for job summaries and PR comments). --json, --sarif and --markdown write extra report files in the same run.
  • --fail-on <severity> sets the threshold (critical, high, medium, low, info, none; default high).
  • Exit codes: 0 passed, 1 findings at or above --fail-on, 2 usage, config or read error.

GitHub Action

permissions:
  contents: read
  security-events: write   # only needed for the SARIF upload
jobs:
  retention-warden:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
        with: { persist-credentials: false }
      - uses: MrLouisWright/retention-warden@v1
        with:
          path: .
          fail-on: high
          sarif: retention-warden.sarif
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with: { sarif_file: retention-warden.sarif }
InputDescriptionDefault
pathPolicy file, or directory containing retention.yml.
fail-onFailure thresholdhigh
configConfig file path
sarif / jsonWrite SARIF / JSON report to this path
job-summaryAdd the Markdown report to the job summarytrue
argsExtra CLI arguments

The action runs the bundled CLI with Node 20; it has no dependencies to install. Pin it to a commit SHA if your policy requires it.

Library

import { check, lintPolicy, parsePeriod } from "retention-warden";
parsePeriod("P2Y"); // { days: 730.5 }
const report = check("retention.yml");
console.log(report.counts, report.findings);

Types are included.

Requirements

Node.js 18 or newer. Zero runtime dependencies.

Support

If this project is useful to you, you can support its development with a donation: Buy me a coffee. It helps keep these tools maintained, documented and free for everyone.

Credit (optional)

Free to use under MIT, no attribution required. If retention-warden helps you, a link back is appreciated:

<a href="https://louiswright.co.uk">Built with retention-warden by Louis Wright</a>
[![Built with retention-warden by Louis Wright](https://img.shields.io/badge/built%20with-retention--warden-blue)](https://louiswright.co.uk)

Licence

MIT © Louis Wright (Louis Wright)