Total SDN Records
12,847
SDN.CSV primary list
SDN Aliases
17,304
ALT.CSV alternate names
Consolidated Primary
15,612
CONS_PRIM.CSV
Consolidated Aliases
21,088
CONS_ALT.CSV
Net Added (7d)
+47
Across all lists
Net Removed (7d)
β12
Across all lists
Last Change Detected
2025β07β14
SDN.CSV updated
Snapshots Stored
284
~90 days history
π Specially Designated Nationals β Primary List
https://sanctionslist.ofac.treas.gov/Home/SdnList β SDN.CSV
β²
Newly Added Entries
23| SDN ID | Name | Type | Program(s) | Country | Remarks |
|---|
βΌ
Removed Entries
5| SDN ID | Name | Type | Program(s) | Country | Remarks |
|---|
π SDN Alternate Names
https://sanctionslist.ofac.treas.gov/Home/SdnList β ALT.CSV
βΉοΈ
No changes detected in latest comparison
β
ALT.CSV matches the previous snapshot exactly.Last checked: 2025-07-14 02:03 UTC Β· Previous change: 2025-07-11 (added 8, removed 2)
π Consolidated Sanctions β Primary List
https://sanctionslist.ofac.treas.gov/Home/ConsolidatedList β CONS_PRIM.CSV
β²
Newly Added Entries
12| ID | Name | Type | Program(s) | Country | DOB |
|---|
βΌ
Removed Entries
7| ID | Name | Type | Program(s) | Country | DOB |
|---|
π Consolidated Sanctions β Alternate Names
https://sanctionslist.ofac.treas.gov/Home/ConsolidatedList β CONS_ALT.CSV
β²
Newly Added Alternate Names
9| Alt ID | Linked SDN ID | Alt Name | Alt Type | Program(s) |
|---|
βοΈ System Architecture & Setup Guide
This dashboard is served by a lightweight Python/Flask app. A systemd timer (or cron job) handles daily downloads and diff computation. Snapshots are stored as gzipped CSV files.
1. Directory Structure
# /opt/ofac-dashboard/ βββ app.py # Flask web server βββ fetch_and_diff.py # Download + diff engine βββ snapshots/ β βββ SDN/ β β βββ 2025-07-13.csv.gz β β βββ 2025-07-14.csv.gz β βββ ALT/ β βββ CONS_PRIM/ β βββ CONS_ALT/ βββ diffs/ β βββ SDN/ β β βββ 2025-07-14.json # added[], removed[] β βββ ... βββ templates/ βββ index.html
2. fetch_and_diff.py
#!/usr/bin/env python3
import requests, gzip, csv, json, hashlib
from pathlib import Path
from datetime import date
LISTS = {
"SDN": "https://sanctionslist.ofac.treas.gov/Home/SdnList/SDN.CSV",
"ALT": "https://sanctionslist.ofac.treas.gov/Home/SdnList/ALT.CSV",
"CONS_PRIM": "https://sanctionslist.ofac.treas.gov/Home/ConsolidatedList/CONS_PRIM.CSV",
"CONS_ALT": "https://sanctionslist.ofac.treas.gov/Home/ConsolidatedList/CONS_ALT.CSV",
}
BASE = Path("/opt/ofac-dashboard")
today = date.today().isoformat()
for name, url in LISTS.items():
snap_dir = BASE / "snapshots" / name
diff_dir = BASE / "diffs" / name
snap_dir.mkdir(parents=True, exist_ok=True)
diff_dir.mkdir(parents=True, exist_ok=True)
r = requests.get(url, timeout=60)
r.raise_for_status()
rows_new = set(r.text.splitlines())
prev = sorted(snap_dir.glob("*.csv.gz"))
rows_old = set()
if prev:
with gzip.open(prev[-1], "rt") as f:
rows_old = set(f.read().splitlines())
added = rows_new - rows_old
removed = rows_old - rows_new
with gzip.open(snap_dir / f"{today}.csv.gz", "wt") as f:
f.write(r.text)
diff = {"date": today, "added": list(added), "removed": list(removed),
"total": len(rows_new), "sha256": hashlib.sha256(r.content).hexdigest()}
with open(diff_dir / f"{today}.json", "w") as f:
json.dump(diff, f)
print(f"{name}: +{len(added)} -{len(removed)}")
3. Systemd Timer (recommended)
# /etc/systemd/system/ofac-fetch.service [Unit] Description=OFAC List Fetch and Diff [Service] Type=oneshot ExecStart=/opt/ofac-dashboard/venv/bin/python /opt/ofac-dashboard/fetch_and_diff.py User=ofac # /etc/systemd/system/ofac-fetch.timer [Unit] Description=Daily OFAC fetch at 02:00 UTC [Timer] OnCalendar=*-*-* 02:00:00 UTC Persistent=true [Install] WantedBy=timers.target # Enable: systemctl enable --now ofac-fetch.timer
4. Alternative: crontab
# crontab -e
0 2 * * * /opt/ofac-dashboard/venv/bin/python /opt/ofac-dashboard/fetch_and_diff.py >> /var/log/ofac-fetch.log 2>&1
5. Flask server (app.py excerpt)
#!/usr/bin/env python3
from flask import Flask, render_template, jsonify
import json
from pathlib import Path
app = Flask(__name__)
BASE = Path("/opt/ofac-dashboard")
@app.route("/")
def index():
data = {}
for name in ["SDN","ALT","CONS_PRIM","CONS_ALT"]:
diffs = sorted((BASE/"diffs"/name).glob("*.json"))
if diffs:
data[name] = json.loads(diffs[-1].read_text())
return render_template("index.html", data=data)
@app.route("/api/history/<list_name>")
def history(list_name):
diffs = sorted((BASE/"diffs"/list_name).glob("*.json"))
return jsonify([json.loads(d.read_text()) for d in diffs[-30:]])
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)