Skip to content

Fetch MSRC CVE Data #37

Fetch MSRC CVE Data

Fetch MSRC CVE Data #37

Workflow file for this run

name: Fetch MSRC CVE Data
on:
schedule:
- cron: '*/30 * * * *' # Automatically runs every half hour
workflow_dispatch: # Manual trigger from GitHub UI
jobs:
fetch:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Fetch and Parse 12 Months of MSRC Data
run: |
mkdir -p data
python3 << 'EOF'
import json
import re
import time
import urllib.request
import urllib.error
from datetime import datetime, date, timezone
def fetch_with_retry(url, retries=3, delay=2):
req = urllib.request.Request(url, headers={'Accept': 'application/json'})
for attempt in range(retries):
try:
with urllib.request.urlopen(req, timeout=15) as resp:
return json.loads(resp.read().decode('utf-8'))
except urllib.error.HTTPError as e:
if e.code == 404:
raise e
print(f"-> HTTP Error {e.code} for {url}. Attempt {attempt + 1}/{retries}...")
except Exception as e:
print(f"-> Connection error: {e} for {url}. Attempt {attempt + 1}/{retries}...")
if attempt < retries - 1:
wait_time = delay * (attempt + 1)
print(f"Waiting {wait_time}s before retry...")
time.sleep(wait_time)
raise Exception(f"Failed to fetch {url} after {retries} attempts")
# Generate last 12 months (oldest first, so newest overwrites in case of revisions)
today = date.today()
month_ids = []
for i in range(11, -1, -1):
m = today.month - i
y = today.year
while m <= 0:
m += 12
y -= 1
month_name = date(y, m, 1).strftime('%b')
month_ids.append(f"{y}-{month_name}")
print("Generating data for months:", month_ids)
cves_map = {}
for month_id in month_ids:
url = f"https://api.msrc.microsoft.com/cvrf/v3.0/cvrf/{month_id}"
print(f"Fetching: {url} ...")
try:
raw = fetch_with_retry(url)
except urllib.error.HTTPError as e:
if e.code == 404:
print(f"-> Month {month_id} not published yet (404). Skipping.")
continue
except Exception as e:
print(f"-> Error fetching {month_id}: {e}. Skipping.")
continue
product_map = {}
def walk(branch):
if not branch:
return
for p in branch.get('FullProductName', []):
product_map[p['ProductID']] = p['Value']
for b in branch.get('Branch', []):
walk(b)
walk(raw.get('ProductTree', {}))
vulns = raw.get('Vulnerability', [])
for v in vulns:
cve_id = v.get('CVE', '')
if not cve_id:
continue
title = ''
if v.get('Title') and v['Title'].get('Value'):
title = v['Title']['Value']
severity = ''
for t in v.get('Threats', []):
if t.get('Type') == 3:
severity = (t.get('Description') or {}).get('Value', '')
if severity:
break
if not severity:
for t in v.get('Threats', []):
if t.get('Type') == 0:
severity = (t.get('Description') or {}).get('Value', '')
if severity:
break
cvss = None
for s in v.get('CVSSScoreSets', []):
if s.get('BaseScore') and ('3.1' in s.get('Vector','') or '3.0' in s.get('Vector','')):
cvss = float(s['BaseScore'])
break
if cvss is None:
for s in v.get('CVSSScoreSets', []):
if s.get('BaseScore'):
cvss = float(s['BaseScore'])
break
products = []
for ps in v.get('ProductStatuses', []):
for pid in ps.get('ProductID', []):
pname = product_map.get(pid, '')
if pname and pname not in products:
products.append(pname)
product = products[0] if products else ''
date_str = ''
revs = v.get('RevisionHistory', [])
if revs:
date_str = revs[0].get('Date', '')[:10]
exploited = False
for t in v.get('Threats', []):
if t.get('Type') == 1:
desc = (t.get('Description') or {}).get('Value', '')
if 'exploited:yes' in desc.lower().replace(' ', ''):
exploited = True
break
kb_map = {}
for r in v.get('Remediations', []):
desc_val = (r.get('Description') or {}).get('Value', '').strip()
subtype_val = r.get('SubType', '').strip()
url_str = r.get('URL', '').strip()
fixed = r.get('FixedBuild', '').strip()
pids = r.get('ProductID', [])
# Robustly extract KB number
kb_num = None
if desc_val.upper().startswith('KB') and desc_val[2:].isdigit():
kb_num = desc_val[2:]
elif desc_val.isdigit() and 6 <= len(desc_val) <= 8:
kb_num = desc_val
elif subtype_val.upper().startswith('KB') and subtype_val[2:].isdigit():
kb_num = subtype_val[2:]
elif subtype_val.isdigit() and 6 <= len(subtype_val) <= 8:
kb_num = subtype_val
else:
m = re.search(r'\b(?:KB)?(\d{6,8})\b', desc_val, re.IGNORECASE)
if m:
kb_num = m.group(1)
else:
m = re.search(r'\b(?:KB)?(\d{6,8})\b', subtype_val, re.IGNORECASE)
if m:
kb_num = m.group(1)
elif url_str:
m = re.search(r'(?:help/|q=KB|q=)(\d{6,8})\b', url_str, re.IGNORECASE)
if m:
kb_num = m.group(1)
else:
m = re.search(r'\b(\d{6,8})\b', url_str)
if m:
kb_num = m.group(1)
if not kb_num:
continue
kb_products = [product_map.get(pid, '') for pid in pids if product_map.get(pid, '')]
def get_url_score(url):
if not url: return 0
u_l = url.lower()
if 'catalog.update.microsoft.com' in u_l: return 4
if 'support.microsoft.com' in u_l: return 3
if 'microsoft.com' in u_l: return 2
return 1
if kb_num not in kb_map:
kb_map[kb_num] = {
'kb': kb_num,
'url': url_str,
'fixed_build': fixed,
'subtype': subtype_val,
'products': kb_products
}
else:
# Merge products
for pname in kb_products:
if pname and pname not in kb_map[kb_num]['products']:
kb_map[kb_num]['products'].append(pname)
# Update URL if higher score
if get_url_score(url_str) > get_url_score(kb_map[kb_num]['url']):
kb_map[kb_num]['url'] = url_str
# Update fixed build if empty
if not kb_map[kb_num]['fixed_build'] and fixed:
kb_map[kb_num]['fixed_build'] = fixed
# Update subtype if new is better
if (not kb_map[kb_num]['subtype'] or kb_map[kb_num]['subtype'].isdigit()) and subtype_val and not subtype_val.isdigit():
kb_map[kb_num]['subtype'] = subtype_val
# Finalize KB map
kb_articles = []
for kb_num, kb_info in kb_map.items():
if not kb_info['url']:
kb_info['url'] = f"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB{kb_num}"
if not kb_info['subtype'] or kb_info['subtype'].isdigit():
kb_info['subtype'] = 'Security Update'
kb_articles.append({
'kb': kb_info['kb'],
'url': kb_info['url'],
'fixed_build': kb_info['fixed_build'],
'subtype': kb_info['subtype'],
'products': kb_info['products'][:8]
})
kb_articles.sort(key=lambda x: x['kb'])
cves_map[cve_id] = {
'cve_id': cve_id,
'title': title,
'severity': severity,
'cvss': cvss,
'product': product,
'products': products[:5],
'date': date_str,
'exploited': exploited,
'kb_articles': kb_articles,
'kb_count': len(kb_articles),
}
parsed = list(cves_map.values())
sev_order = {'Critical':0, 'Important':1, 'Moderate':2, 'Low':3}
parsed.sort(key=lambda x: (
0 if x['exploited'] else 1,
sev_order.get(x['severity'], 4),
-(x['cvss'] or 0),
x['cve_id']
))
now = datetime.now(timezone.utc)
counts = {
'total': len(parsed),
'critical': sum(1 for x in parsed if x['severity'] == 'Critical'),
'important': sum(1 for x in parsed if x['severity'] == 'Important'),
'moderate': sum(1 for x in parsed if x['severity'] == 'Moderate'),
'low': sum(1 for x in parsed if x['severity'] == 'Low'),
'exploited': sum(1 for x in parsed if x['exploited']),
'patched': sum(1 for x in parsed if x['kb_count'] > 0),
'updated_at': now.strftime('%Y-%m-%d %H:%M UTC'),
'month': "Past 12 Months"
}
output = {'meta': counts, 'cves': parsed}
with open('data/msrc_cves.json', 'w', encoding='utf-8') as f:
json.dump(output, f, ensure_ascii=False, indent=2)
print(f"Success: {counts['total']} CVEs consolidated. Critical: {counts['critical']} | Exploited: {counts['exploited']} | Patched: {counts['patched']}")
EOF
- name: Commit updated data
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add data/msrc_cves.json
git diff --staged --quiet || git commit -m "chore: update rolling 12-month MSRC CVE data [$(date -u +'%Y-%m-%d %H:%M UTC')]"
git push