Preskočiť na obsah

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 checkout alebo 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 pull musí 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:

git clone --depth 1 \
  https://github.com/CVEProject/cvelistV5.git \
  cvelistv5-depth1.git

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 sparse-checkout init --cone
git worktree add --detach ../cves-2026 main
> 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.

( cd ../cvelistv5-sparse.git/ && git fetch origin main ); git checkout origin/main

Š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:
> cat .git
gitdir: /tmp/cvelistv5-sparse.git/.git/worktrees/cves-2026

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