#!/usr/bin/env python3 """Confronta le statistiche scritte nelle schede con quelle attuali. Sola lettura: non modifica nessuna scheda, mostra solo che cosa è cambiato. Per ogni scheda controlla, quando la scheda li riporta: - stelle, fork, licenza e ultima versione del repository (GitHub, tramite `gh`) - installazioni e controlli di sicurezza (pagina della skill su skills.sh) Uso: python3 scripts/controlla-statistiche.py # tutte le schede, solo le differenze python3 scripts/controlla-statistiche.py --tutto # mostra anche i valori invariati python3 scripts/controlla-statistiche.py --soglia 5 pdf # scarto minimo 5%, solo la scheda pdf Serve `gh` con l'accesso effettuato (`gh auth login`). Uscita 1 se c'è qualcosa da aggiornare. """ import argparse import json import re import subprocess import sys import urllib.request from concurrent.futures import ThreadPoolExecutor from pathlib import Path SCHEDE = Path(__file__).resolve().parent.parent / "skills" AUDITOR = ["Gen Agent Trust Hub", "Socket", "Snyk"] MESI = ["gennaio", "febbraio", "marzo", "aprile", "maggio", "giugno", "luglio", "agosto", "settembre", "ottobre", "novembre", "dicembre"] def num_it(cifre, milioni=None): """'35.200' -> 35200; '3,9' + 'milioni' -> 3900000.""" if milioni: return round(float(cifre.replace(",", ".")) * 1_000_000) return int(cifre.replace(".", "").replace(",", "")) def num_sksh(testo): """'219.7K' -> 219700; '1.4M' -> 1400000; '649' -> 649.""" mult = {"K": 1_000, "M": 1_000_000}.get(testo[-1].upper(), 1) return round(float(testo.rstrip("KkMm")) * mult) def fmt(n): return f"{n:,}".replace(",", ".") def gh(percorso): r = subprocess.run(["gh", "api", percorso], capture_output=True, text=True) return json.loads(r.stdout) if r.returncode == 0 else None def pagina(url): try: req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) html = urllib.request.urlopen(req, timeout=25).read().decode("utf-8", "replace") except Exception: return None html = re.sub(r"<(script|style).*?", " ", html, flags=re.S) return re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", html)) def audit_scheda(testo): """Esiti dei controlli come li dice la scheda: {'Socket': 'pass' | 'warn'}.""" m = re.search(r"\*\*Controlli di sicurezza su skills\.sh:\*\*(.*)", testo) if not m: return {} riga = re.sub(r"\([^)]*\)", "", m.group(1)) esiti = {} for clausola in re.split(r"[;.]", riga): nomi = [a for a in AUDITOR if a in clausola] if not nomi: continue if re.search(r"avviso|segnala|non superat", clausola): stato = "warn" elif "supera" in clausola: stato = "pass" else: continue esiti.update({a: stato for a in nomi}) return esiti def confronta(righe, nome, scheda, ora, soglia): """Aggiunge una riga numerica; segnala se lo scarto supera la soglia.""" scarto = (ora - scheda) / scheda * 100 if scheda else 100.0 esito = "DA AGGIORNARE" if abs(scarto) >= soglia else "ok" righe.append((nome, fmt(scheda), fmt(ora), f"{scarto:+.1f}%", esito)) def controlla(percorso, soglia): testo = percorso.read_text(encoding="utf-8") righe = [] repo = re.search(r"\*\*Repository:\*\*\s*\[[^\]]*\]\(https://github\.com/([^/\s)]+/[^/\s)]+)\)", testo) riga_repo = re.search(r"\*\*Repository:\*\*(.*)", testo) riga_repo = riga_repo.group(1) if riga_repo else "" # GitHub: stelle, fork, licenza, versione if repo: info = gh(f"repos/{repo.group(1)}") if info is None: righe.append(("repository", repo.group(1), "non raggiungibile", "", "DA CONTROLLARE")) else: if m := re.search(r"circa ([\d.,]+)( milioni)? stelle", testo): confronta(righe, "stelle", num_it(*m.groups()), info["stargazers_count"], soglia) if m := re.search(r"([\d.,]+)( milioni)? fork", testo): confronta(righe, "fork", num_it(*m.groups()), info["forks_count"], soglia) if m := re.search(r"licenza ([A-Za-z0-9.\-]+)", riga_repo): ora = (info.get("license") or {}).get("spdx_id") or "nessuna" esito = "ok" if ora.lower() == m.group(1).lower() else "DA AGGIORNARE" righe.append(("licenza", m.group(1), ora, "", esito)) if m := re.search(r"versione v?(\d+(?:\.\d+)+)", riga_repo): rel = gh(f"repos/{repo.group(1)}/releases/latest") if rel: ora = rel["tag_name"].lstrip("v") esito = "ok" if ora == m.group(1) else "DA AGGIORNARE" righe.append(("versione", m.group(1), ora, "", esito)) if info.get("archived"): righe.append(("repository", "", "archiviato", "", "DA CONTROLLARE")) # skills.sh: installazioni e controlli di sicurezza sksh = re.search(r"\*\*Pagina su skills\.sh:\*\*\s*\[[^\]]*\]\((https://skills\.sh/[^)\s]+)\)", testo) if sksh: pg = pagina(sksh.group(1)) if pg is None: righe.append(("skills.sh", sksh.group(1), "non raggiungibile", "", "DA CONTROLLARE")) else: m_sc = re.search(r"circa ([\d.,]+)( milioni)? installazioni", testo) m_pg = re.search(r"([\d.,]+[KM]?) total installs", pg) or re.search(r"\bInstalls ([\d.,]+[KM]?)(?=\s)", pg) if m_sc and m_pg: confronta(righe, "installazioni", num_it(*m_sc.groups()), num_sksh(m_pg.group(1)), soglia) attesi = audit_scheda(testo) visti = {a: ("pass" if s == "Pass" else "warn") for a, s in re.findall(r"(Gen Agent Trust Hub|Socket|Snyk) (Pass|Warn|Fail)", pg)} for a in AUDITOR: if a in attesi and a in visti: esito = "ok" if attesi[a] == visti[a] else "DA AGGIORNARE" righe.append((f"sicurezza: {a}", attesi[a], visti[a], "", esito)) return righe def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("schede", nargs="*", help="nomi delle schede da controllare (predefinito: tutte)") ap.add_argument("--soglia", type=float, default=10.0, help="scarto %% oltre il quale segnalare (predefinito 10)") ap.add_argument("--tutto", action="store_true", help="mostra anche i valori invariati") args = ap.parse_args() elenco = [p for p in sorted(SCHEDE.glob("*.md")) if not args.schede or p.stem in args.schede] if not elenco: sys.exit("nessuna scheda trovata") with ThreadPoolExecutor(max_workers=6) as pool: risultati = list(pool.map(lambda p: controlla(p, args.soglia), elenco)) da_aggiornare = 0 for percorso, righe in zip(elenco, risultati): mostra = [r for r in righe if args.tutto or r[4] != "ok"] da_aggiornare += sum(r[4] != "ok" for r in righe) if not mostra: continue rilevazione = re.search(r"Diffusione \(([^)]+)\)", percorso.read_text(encoding="utf-8")) print(f"{percorso.stem} (rilevazione: {rilevazione.group(1) if rilevazione else 'non indicata'})") for nome, scheda, ora, scarto, esito in mostra: print(f" {nome:<24} scheda {scheda:>12} ora {ora:>12} {scarto:>7} {esito}") print(f"\n{len(elenco)} schede controllate, {da_aggiornare} voci da aggiornare (soglia {args.soglia:g}%)") sys.exit(1 if da_aggiornare else 0) if __name__ == "__main__": main()