Put a legal hold on a deletion job and prove it held
Task
Build a small records store with a retention schedule that deletes expired records automatically, then place a legal hold on one matter and prove that the job now spares the held records while still deleting everything else that has expired. A hold that exists only as an email to staff is the one that loses a court case, because the automated job nobody paused deleted the evidence.
Steps
- Create
/tmp/recordsand fill it with at least twenty synthetic record files. Give at least ten of them a modification time over a year old withtouch -d '500 days ago' <file>, and leave the rest recent. Name at least three of the old ones with the matter referenceACME, for exampleinvoice-ACME-0012.txt. - Write
/tmp/retention.sh, the retention schedule: it deletes files in/tmp/recordsolder than 365 days, but first reads/tmp/holds.txt(one matter reference per line) and skips any file whose name contains a held reference. Usefind /tmp/records -type f -mtime +365to select candidates; never point it anywhere else. - Litigation over the ACME matter is now reasonably expected. Write
ACMEinto/tmp/holds.txt, and write/tmp/hold-notice.md: the matter, the scope of data held, the custodians notified, the date, and every automated deletion you would pause in a real estate (mailbox purges, log rotation, backup expiry). - Run
bash /tmp/retention.sh. - Confirm by listing what is left: the old ACME records survive, the other expired records are gone, and the recent records are untouched.
- In
/tmp/hold-notice.md, write one sentence on what releases the hold and what happens to the held records afterwards.
Verify
find /tmp/records -type f -mtime +365 | grep -c ACME
python3 - <<'PY'
import os,subprocess,time
d='/tmp/records'
held=open('/tmp/holds.txt').read().split()
assert held, '/tmp/holds.txt names no matter'
old=time.time()-500*86400
for name in ('verify-unheld.txt','verify-%s.txt' % held[0]):
p=os.path.join(d,name); open(p,'w').write('synthetic record\n'); os.utime(p,(old,old))
subprocess.run(['bash','/tmp/retention.sh'],check=True)
left=set(os.listdir(d))
assert 'verify-unheld.txt' not in left, 'an expired record under no hold survived - the schedule is not deleting'
assert 'verify-%s.txt' % held[0] in left, 'an expired record under hold was deleted - the hold did not stop the job'
expired=[f for f in left if time.time()-os.path.getmtime(os.path.join(d,f))>365*86400]
stray=[f for f in expired if not any(h in f for h in held)]
assert not stray, 'expired, unheld records remain: '+', '.join(stray)
recent=[f for f in left if f not in expired]
assert len(recent)>=5, 'recent records were deleted too - the schedule is too broad'
print('%d held record(s) preserved, %d recent untouched, no expired unheld record left' % (len(expired),len(recent)))
PY
grep -ciE "custodian|backup|log rotation|mailbox" /tmp/hold-notice.md
The Verify plants two fresh expired records of its own, one under the hold and one not, and runs your job again. That makes it a test of the job rather than of the directory you left behind: the held record must survive and the unheld one must go. The first command must print at least three.
Notes
The hold notice is the half people write and the paused job is the half they forget. In a real estate the deletion is spread across mailbox retention policies, log rotation, backup expiry and SaaS defaults, each owned by a different team, which is why a legal hold needs a named list of the automated jobs it suspends and somebody who confirms each one.
This is an independent study companion for CompTIA Security+ SY0-801 and is not produced by or endorsed by CompTIA.