Initial Builder
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build compact CVE SQLite plus optionally incremental row-level delta (CVE slice)."""
|
||||
import argparse
|
||||
import base64
|
||||
import datetime as dt
|
||||
import hashlib
|
||||
import json
|
||||
import sqlite3
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
SCHEMA = '''CREATE TABLE cve (
|
||||
cve_id TEXT PRIMARY KEY,
|
||||
state TEXT NOT NULL,
|
||||
date_updated TEXT,
|
||||
affected BLOB NOT NULL,
|
||||
row_hash TEXT NOT NULL
|
||||
) WITHOUT ROWID;
|
||||
CREATE INDEX idx_cve_state ON cve(state);'''
|
||||
|
||||
def encode_record(doc):
|
||||
meta = doc['cveMetadata']
|
||||
cna = doc.get('containers', {}).get('cna', {})
|
||||
# Preserve entire CNA affected array as structured JSON. Do NOT infer ranges.
|
||||
affected = json.dumps(cna.get('affected', []), sort_keys=True, separators=(',', ':'), ensure_ascii=False).encode('utf-8')
|
||||
blob = zlib.compress(affected, level=9)
|
||||
row = (meta['cveId'], meta.get('state', 'UNKNOWN'), meta.get('dateUpdated'), blob)
|
||||
digest = hashlib.sha256(json.dumps([row[0], row[1], row[2]], separators=(',', ':')).encode() + blob).hexdigest()
|
||||
return (*row, digest)
|
||||
|
||||
def fingerprint(conn):
|
||||
h = hashlib.sha256()
|
||||
for cve_id, row_hash in conn.execute('SELECT cve_id,row_hash FROM cve ORDER BY cve_id'):
|
||||
h.update(cve_id.encode() + b'\x00' + row_hash.encode() + b'\n')
|
||||
return h.hexdigest()
|
||||
|
||||
def export_zstd(path, output):
|
||||
with output.open('wb') as dst:
|
||||
subprocess.run(['zstd', '-q', '-12', '-c', str(path)], stdout=dst, check=True)
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--source', type=Path, required=True, help='vuln-cve/data')
|
||||
ap.add_argument('--output', type=Path, default=Path('dist'))
|
||||
ap.add_argument('--previous', type=Path, help='previous SQLite master.db')
|
||||
ap.add_argument('--source-revision', default='unknown')
|
||||
args = ap.parse_args()
|
||||
args.output.mkdir(parents=True, exist_ok=True)
|
||||
db = args.output / 'master.db'
|
||||
db.unlink(missing_ok=True)
|
||||
current = sqlite3.connect(db)
|
||||
current.executescript(SCHEMA)
|
||||
count = 0
|
||||
for path in sorted(args.source.rglob('CVE-*.json')):
|
||||
with path.open(encoding='utf-8') as fh:
|
||||
doc = json.load(fh)
|
||||
if doc.get('cveMetadata', {}).get('cveId') != path.stem:
|
||||
raise ValueError(f'CVE metadata mismatch at {path}')
|
||||
current.execute('INSERT INTO cve VALUES (?,?,?,?,?)', encode_record(doc))
|
||||
count += 1
|
||||
if count % 5000 == 0:
|
||||
current.commit()
|
||||
if count == 0:
|
||||
raise RuntimeError('No CVE records, refusing empty release')
|
||||
current.commit()
|
||||
current.execute('VACUUM')
|
||||
current.commit()
|
||||
current_hash = fingerprint(current)
|
||||
previous_hash = None
|
||||
changed = removed = 0
|
||||
if args.previous:
|
||||
prev = sqlite3.connect(f'file:{args.previous.resolve()}?mode=ro', uri=True)
|
||||
previous_hash = fingerprint(prev)
|
||||
old = {row[0]: row[1] for row in prev.execute('SELECT cve_id,row_hash FROM cve')}
|
||||
delta_file = args.output / 'delta.jsonl.zst'
|
||||
plain_delta = args.output / 'delta.jsonl'
|
||||
with plain_delta.open('w', encoding='utf-8') as out:
|
||||
header = {'type': 'header', 'format': 1, 'base': previous_hash, 'target': current_hash}
|
||||
out.write(json.dumps(header) + '\n')
|
||||
for id, state, date, blob, row_hash in current.execute('SELECT * FROM cve ORDER BY cve_id'):
|
||||
old_hash = old.pop(id, None)
|
||||
if old_hash != row_hash:
|
||||
change = {'type': 'upsert', 'id': id, 'state': state, 'updated': date, 'affected_b64': base64.b64encode(blob).decode(), 'hash': row_hash}
|
||||
out.write(json.dumps(change, separators=(',', ':')) + '\n')
|
||||
changed += 1
|
||||
for id in sorted(old):
|
||||
out.write(json.dumps({'type': 'delete', 'id': id}) + '\n')
|
||||
removed += 1
|
||||
export_zstd(plain_delta, delta_file)
|
||||
plain_delta.unlink()
|
||||
prev.close()
|
||||
export_zstd(db, args.output / 'master.db.zst')
|
||||
manifest = {
|
||||
'format': 1,
|
||||
'schema': 'cve-index-v1',
|
||||
'built_at': dt.datetime.now(dt.timezone.utc).isoformat(),
|
||||
'sources': {'cve': args.source_revision},
|
||||
'records': count,
|
||||
'content_sha256': current_hash,
|
||||
'previous_content_sha256': previous_hash,
|
||||
'upserts': changed,
|
||||
'deletions': removed,
|
||||
'artifacts': {},
|
||||
}
|
||||
for f in sorted(args.output.glob('*.zst')):
|
||||
manifest['artifacts'][f.name] = {'sha256': hashlib.sha256(f.read_bytes()).hexdigest(), 'bytes': f.stat().st_size}
|
||||
(args.output / 'manifest.json').write_text(json.dumps(manifest, indent=2) + '\n')
|
||||
print(json.dumps(manifest, indent=2))
|
||||
current.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user