#!/usr/bin/env python3
"""Mesure les en-tetes de securite HTTP des sites de la verticale VPN / vie privee.

Meme methode que le jeu de donnees publie sous DOI 10.5281/zenodo.21948938
(en-tetes de securite de 500 sites d'administrations publiques francaises).

Ce que ce score mesure : la presence d'en-tetes HTTP de securite sur la page
d'accueil, et RIEN D'AUTRE. Ce n'est PAS un audit de securite du produit.
Un score faible ne dit rien de la solidite du logiciel : il dit que le site
web ne declare pas ces en-tetes.

Sortie : CSV sur stdout.
Licence : MIT. Reproductible : relancer donne l'etat du jour.

Usage : python3 security-headers-scan.py sites.txt > resultats.csv
(un domaine par ligne, sans schema)
"""
import urllib.request, ssl, csv, sys, datetime

UA = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
                    "(KHTML, like Gecko) Chrome/126 Safari/537.36"}

# Poids proportionnels a ce que l'en-tete empeche reellement.
# CSP et HSTS pesent le plus : ils bloquent des classes entieres d'attaque.
CHECKS = [
    ("content-security-policy", 25),      # bloque l'injection de script
    ("strict-transport-security", 20),    # interdit la degradation vers HTTP
    ("x-content-type-options", 10),       # empeche le MIME sniffing
    ("x-frame-options", 10),              # anti-clickjacking
    ("referrer-policy", 10),              # limite la fuite d'URL vers les tiers
    ("permissions-policy", 10),           # restreint camera/micro/geoloc
    ("cross-origin-opener-policy", 10),   # isole le contexte de navigation
    ("cross-origin-resource-policy", 5),  # limite l'inclusion cross-origin
]


def grade(score):
    if score >= 90: return "A+"
    if score >= 75: return "A"
    if score >= 60: return "B"
    if score >= 40: return "C"
    if score >= 20: return "D"
    return "F"


def main():
    sites = [l.strip() for l in open(sys.argv[1]) if l.strip()]  # usage: scan.py sites.txt > out.csv
    stamp = datetime.date.today().isoformat()
    ctx = ssl.create_default_context()
    w = csv.writer(sys.stdout)
    w.writerow(["site", "score", "grade"] + [c for c, _ in CHECKS] + ["http_status", "checked_at"])
    for s in sites:
        try:
            r = urllib.request.urlopen(urllib.request.Request("https://" + s + "/", headers=UA),
                                       timeout=25, context=ctx)
            h = {k.lower(): v for k, v in r.headers.items()}
            code = r.status
        except Exception as e:
            # Une erreur de collecte n'est PAS un score de zero : champ vide.
            w.writerow([s, "", "", *[""] * len(CHECKS), "ERR:" + type(e).__name__, stamp])
            sys.stdout.flush()
            continue
        vals, score = [], 0
        for c, p in CHECKS:
            present = c in h
            # frame-ancestors dans la CSP remplace legitimement X-Frame-Options
            if not present and c == "x-frame-options":
                present = "frame-ancestors" in h.get("content-security-policy", "")
            vals.append("1" if present else "0")
            if present:
                score += p
        w.writerow([s, score, grade(score), *vals, code, stamp])
        sys.stdout.flush()


if __name__ == "__main__":
    main()
