Ako skrotiť 400-tisíc CVE súborov v Gite bez preťaženia disku
Projekt CVEProject/cvelistV5 sleduje zraniteľnosti (CVE) už od roku 1999. Dnes obsahuje viac ako 400 tisíc JSON súborov, pričom ten najväčší prírastok dát prichádza s každým novým rokom.
Pre sysadminov, DevOps inžinierov a bezpečákov predstavuje takýto objem dát v jednom repozitári celkom slušnú záťaž:
- Brzda pre Git index: Spracovanie 400k súborov pri operáciách ako
git status,git checkoutalebo pri otváraní adresára v IDE spôsobuje značné spomalenie. - Spotreba diskového priestoru: Väčšinu starých ročníky na dennú prácu vôbec nepotrebujeme
- Pomalá synchronizácia: Bežný
git pullmusí zbytočne prechádzať obrovský strom objektov.
Pozrime sa na to, ako tento problém elegantne vyriešiť bez zbytočnej réžie.
Možné prístupy¶
Oficiálne riešenie¶
Oficiálny repozitár používa hlavne git tags a denné/hodinové commit-batchingy. Priamo v dokumentácií projektu nie je uvedená žiadna optimalizácia, iba info, ze projekt možno klonovať cez git.
Po vyklonovaní projektu nám pribudne na disku takmer 8 GB dát. Väčšinu súborov však my už nepotrebujeme.
Veľkosť adresárov podla rokov:
for year in $(seq 1999 2026); do
files=$(find $year -type f -name "*.json" | wc -l | cut -d' ' -f 1);
size=$(du -chs $year | head -n 1 | cut -f 1 );
echo "| $year | $size | $files |";
done
%%{init: {
'theme': 'base',
'themeVariables': {
'xyChart': {
'backgroundColor': '#ffffff',
'titleColor': '#1e293b',
'xAxisLabelColor': '#475569',
'xAxisTitleColor': '#0f172a',
'yAxisLabelColor': '#475569',
'yAxisTitleColor': '#0f172a',
'plotColorPalette': '#2563eb, #10b981, #f59e0b, #ef4444'
}
}
}}%%
xychart-beta
title "Number of CVEs (1999–2026)"
x-axis ["2020", "2021", "2022", "2023", "2024", "2025", "2026"]
y-axis "Number of CVEs" 0 --> 85000
bar [21086, 23481, 27563, 31473, 39358, 45428, 79867] Trend vyzerá pomerne znepokojujúco:
Shallow clone¶
Z iných projektov teda na optimalizáciu možno poznáte Shallow Clone. Používa sa to napr pri klonovaní v github pipelines:
Iniciálny klon repozitára ušetrí viac ako polovicu času aj objem prenesených dát. Ale takýto pull je vhodný iba na jednorázové akcie, kde sa dátami už ďalej nepracuje (napr. v rámci GitHub pipeline). Ak uvažujeme o pravidelnom doťahovaní zmien, tak vzniká problém pri prvej aktualizácii:
> cd cvelistv5-depth1.git
> git pull
remote: Enumerating objects: 2045455, done.
remote: Counting objects: 100% (2045455/2045455), done.
remote: Compressing objects: 100% (98160/98160), done.
Receiving objects: 8% (166176/2000611), 252.35 MiB | 5.68 MiB/s
^^ ^^^^^^^-- PROBLEM !
Prvý pull stiahne aj ostatné blob objekty z histórie a sme teda v rovnakom stave, ako pri full clone.
Sparse Checkout¶
Git disponuje pokročilými funkciami na filtrovane blob objektov, ktoré nás nezaujímajú. Skúsme teda vyklonovať projekt takto:
git clone --filter=blob:none --no-checkout \
https://github.com/CVEProject/cvelistV5.git \
cvelistv5-sparse.git
> git clone --filter=blob:none --no-checkout https://github.com/CVEProject/cvelistV5.git cvelistv5-sparse.git
Cloning into 'cvelistv5-sparse.git'...
remote: Enumerating objects: 615593, done.
remote: Counting objects: 100% (453/453), done.
remote: Compressing objects: 100% (161/161), done.
remote: Total 615593 (delta 360), reused 296 (delta 292), pack-reused 615140 (from 4)
Receiving objects: 100% (615593/615593), 341.12 MiB | 3.70 MiB/s, done.
Resolving deltas: 100% (563653/563653), done.
Ako vidíme, prenieslo sa výrazne menej git objektov, iba git index. Zatiaľ ešte nemáme samotné JSON súbory, takže poďme ich stiahnuť:
> git worktree add --detach ../cves-2026 main
Preparing worktree (detached HEAD 266f47815a)
remote: Enumerating objects: 79872, done.
remote: Counting objects: 100% (73462/73462), done.
remote: Compressing objects: 100% (34904/34904), done.
remote: Total 79872 (delta 72235), reused 38558 (delta 38558), pack-reused 6410 (from 4)
Receiving objects: 100% (79872/79872), 77.09 MiB | 3.85 MiB/s, done.
Resolving deltas: 100% (78399/78399), done.
Updating files: 100% (79872/79872), done.
HEAD is now at 266f47815a 1 changes (1 new | 0 updated): - 1 new CVEs: CVE-2026-116050 - 0 updated CVEs:
Keď sa pozriete podrobne, tak sme 8 GB dát zredukovali na 1.4 GB - 492 MB index, 842 MB JSON súbory.
Porovnanie prístupov¶
| metóda | full clone | shallow clone | sparse clone |
|---|---|---|---|
| celková veľkosť | 7.3 GB | 4.5 GB | 1.4 GB |
veľkosť .git/ | 3.2 GB | 3.4 GB | 492 MB |
| celkový počet súborov | 414030 | 447934 | 79918 |
| čas na prvý clone | 23:45 | 4:44 | 3:18 |
Pravidelný update¶
Kedže sme fyzicky oddelili dáta a git index do samostatných adresárov, tak update musíme spraviť dvojkrokovo.
To je menšia komplikácia nášho prístupu, lebo musíme najprv aktualizovať index a potom spraviť checkout.
Štruktúra vyzerá nasledovne:
> tree -a -L 2 cvelistv5-sparse.git cves-2026/
cvelistv5-sparse.git
└── .git
├── branches
├── config
├── config.worktree
├── description
├── FETCH_HEAD
├── HEAD
├── hooks
├── info
├── logs
├── objects
├── ORIG_HEAD
├── packed-refs
├── refs
└── worktrees
cves-2026/
├── cves
│ ├── delta.json
│ ├── deltaLog.json
│ └── 2026
├── .git
├── .gitattributes
├── .gitignore
└── README.md
12 directories, 13 files
Informácie o našich worktree si git pamätá v súbore
.git/worktrees/<worktree_name>/config.worktree: > cat cvelistv5-sparse.git/.git/worktrees/cves-2026/config.worktree
[core]
sparseCheckout = true
sparseCheckoutCone = true
Ešte jeden drobný detail. V pracovných git projektoch ste možno zvyknutí, že
.git je adresár. V tomto prípade je .git súbor, ktorý obsahuje informáciu o $GIT_DIR: Automatizačný Bash skript¶
Nasledujúci skript automatizuje kombináciu Partial Clone + Sparse Checkout + Worktree. Automaticky detekuje aktuálny kalendárny rok, vytvorí preň samostatný adresár a zabezpečí bezproblémovú synchronizáciu.
#!/usr/bin/env bash
set -euo pipefail
# === CONFIGURATION ===
UPSTREAM_URL="https://github.com/CVEProject/cvelistV5.git"
CORE_DIR="cvelist-core"
# Base set of historical years; current year will be dynamically appended
YEARS_TO_SETUP=(2026)
CURRENT_YEAR=$(date +%Y)
# Add current year if not already in array
if [[ ! " ${YEARS_TO_SETUP[*]} " =~ " ${CURRENT_YEAR} " ]]; then
YEARS_TO_SETUP+=("$CURRENT_YEAR")
fi
# Helper function to create a worktree for a specific year
ensure_worktree() {
local YEAR="$1"
local TARGET_DIR="../cves-$YEAR"
if [ -d "$TARGET_DIR" ]; then
return 0
fi
echo "--> Creating worktree for year $YEAR in $TARGET_DIR..."
git worktree add --detach "$TARGET_DIR" main
(
cd "$TARGET_DIR"
git sparse-checkout set "cves/$YEAR"
)
}
# === FUNCTION: INITIALIZATION ===
do_setup() {
echo "===> 1. Cloning repository metadata (Partial Clone)..."
if [ ! -d "$CORE_DIR" ]; then
git clone --filter=blob:none --no-checkout "$UPSTREAM_URL" "$CORE_DIR"
else
echo "Directory $CORE_DIR already exists, skipping clone."
fi
cd "$CORE_DIR"
git sparse-checkout init --cone
echo "===> 2. Creating sparse-checkout worktrees (Target years: ${YEARS_TO_SETUP[*]})..."
for YEAR in "${YEARS_TO_SETUP[@]}"; do
ensure_worktree "$YEAR"
done
echo ""
echo "===> Initialization complete. Created directories:"
ls -d ../cves-* 2>/dev/null || true
}
# === FUNCTION: SYNCHRONIZATION ===
do_sync() {
if [ ! -d "$CORE_DIR" ]; then
echo "Error: Main directory $CORE_DIR does not exist. Run setup first." >&2
exit 1
fi
echo "===> 1. Fetching latest commits from upstream..."
(
cd "$CORE_DIR"
git fetch origin main
)
echo "===> 2. Ensuring worktree exists for current year ($CURRENT_YEAR)..."
(
cd "$CORE_DIR"
ensure_worktree "$CURRENT_YEAR"
)
echo "===> 3. Updating all created worktree directories..."
for WORKTREE_DIR in ../cves-*; do
if [ -d "$WORKTREE_DIR" ]; then
YEAR=$(basename "$WORKTREE_DIR" | sed 's/cves-//')
echo "--> Synchronizing year $YEAR ($WORKTREE_DIR)..."
(
cd "$WORKTREE_DIR"
git checkout --detach origin/main
)
fi
done
echo ""
echo "===> Synchronization completed successfully!"
}
# === EXECUTION LOGIC ===
COMMAND="${1:-setup}"
case "$COMMAND" in
setup)
do_setup
;;
sync)
do_sync
;;
*)
echo "Usage: $0 {setup|sync}"
exit 1
;;
esac