wopt 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
wopt/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """wopt — mini audit de sécurité web en ligne de commande."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from wopt.core.scanner import scan
6
+
7
+ __all__ = ["scan", "__version__"]
@@ -0,0 +1,36 @@
1
+ """
2
+ wopt.checks
3
+
4
+ Registre central des checks disponibles. Pour ajouter un nouveau
5
+ check : créer la classe dans un module de ce package, puis
6
+ l'ajouter à ALL_CHECKS. Le scanner n'a rien d'autre à connaître.
7
+ """
8
+
9
+ from wopt.checks.cookies import CookiesCheck
10
+ from wopt.checks.cors import CORSCheck
11
+ from wopt.checks.exposure import ExposurePathsCheck
12
+ from wopt.checks.headers import SecurityHeadersCheck
13
+ from wopt.checks.tls import TLSCheck
14
+
15
+ # Checks "standards" exécutés directement sur le ScanContext
16
+ ALL_CHECKS = [
17
+ SecurityHeadersCheck,
18
+ TLSCheck,
19
+ CookiesCheck,
20
+ CORSCheck,
21
+ ]
22
+
23
+ # Checks nécessitant des requêtes HTTP additionnelles (probing actif)
24
+ PROBE_CHECKS = [
25
+ ExposurePathsCheck,
26
+ ]
27
+
28
+ __all__ = [
29
+ "ALL_CHECKS",
30
+ "PROBE_CHECKS",
31
+ "SecurityHeadersCheck",
32
+ "TLSCheck",
33
+ "CookiesCheck",
34
+ "CORSCheck",
35
+ "ExposurePathsCheck",
36
+ ]
wopt/checks/base.py ADDED
@@ -0,0 +1,29 @@
1
+ """
2
+ wopt.checks.base
3
+
4
+ Contrat que tout check de sécurité doit respecter.
5
+ Chaque nouveau check hérite de Check et implémente run().
6
+ Le scanner orchestrateur n'a besoin de rien connaître de la
7
+ logique interne de chaque check : il les découvre et les exécute
8
+ tous de la même façon.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from abc import ABC, abstractmethod
14
+
15
+ from wopt.models import Finding, ScanContext
16
+
17
+
18
+ class Check(ABC):
19
+ """Classe abstraite : tout check de sécurité l'implémente."""
20
+
21
+ id: str = "base.check"
22
+ name: str = "Base Check"
23
+ category: str = "general" # headers | tls | cookies | cors | exposure
24
+
25
+ @abstractmethod
26
+ def run(self, ctx: ScanContext) -> list[Finding]:
27
+ """Exécute la vérification et retourne une liste de Finding.
28
+ Une liste vide = rien à signaler (tout est conforme)."""
29
+ raise NotImplementedError
wopt/checks/cookies.py ADDED
@@ -0,0 +1,58 @@
1
+ """
2
+ wopt.checks.cookies
3
+
4
+ Vérifie que chaque cookie détecté sur la réponse HTTP est
5
+ correctement sécurisé : Secure, HttpOnly, SameSite.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from wopt.checks.base import Check
11
+ from wopt.models import Finding, ScanContext, Severity
12
+
13
+
14
+ class CookiesCheck(Check):
15
+ id = "cookies.flags"
16
+ name = "Cookie Security Flags"
17
+ category = "cookies"
18
+
19
+ def run(self, ctx: ScanContext) -> list[Finding]:
20
+ findings: list[Finding] = []
21
+
22
+ for cookie in ctx.cookies:
23
+ if not cookie.secure:
24
+ findings.append(
25
+ Finding(
26
+ check_id=f"{self.id}.{cookie.name}_not_secure",
27
+ title=f"Cookie '{cookie.name}' sans flag Secure",
28
+ severity=Severity.MEDIUM,
29
+ category=self.category,
30
+ description="Ce cookie peut être transmis en clair sur une connexion non chiffrée.",
31
+ recommendation=f"Ajouter le flag Secure au cookie '{cookie.name}'.",
32
+ )
33
+ )
34
+ if not cookie.http_only:
35
+ findings.append(
36
+ Finding(
37
+ check_id=f"{self.id}.{cookie.name}_not_httponly",
38
+ title=f"Cookie '{cookie.name}' sans flag HttpOnly",
39
+ severity=Severity.MEDIUM,
40
+ category=self.category,
41
+ description="Ce cookie est accessible via JavaScript, ce qui facilite son vol en cas de XSS.",
42
+ recommendation=f"Ajouter le flag HttpOnly au cookie '{cookie.name}'.",
43
+ )
44
+ )
45
+ if not cookie.same_site or cookie.same_site.lower() == "none":
46
+ findings.append(
47
+ Finding(
48
+ check_id=f"{self.id}.{cookie.name}_samesite_weak",
49
+ title=f"Cookie '{cookie.name}' avec SameSite absent ou 'None'",
50
+ severity=Severity.LOW,
51
+ category=self.category,
52
+ description="Ce cookie peut être envoyé lors de requêtes cross-site, augmentant le risque CSRF.",
53
+ recommendation=f"Définir 'SameSite=Lax' ou 'Strict' sur le cookie '{cookie.name}'.",
54
+ evidence=cookie.same_site or "absent",
55
+ )
56
+ )
57
+
58
+ return findings
wopt/checks/cors.py ADDED
@@ -0,0 +1,60 @@
1
+ """
2
+ wopt.checks.cors
3
+
4
+ Détecte les configurations CORS dangereuses, en particulier
5
+ la combinaison Access-Control-Allow-Origin: * associée à
6
+ Access-Control-Allow-Credentials: true, qui permet à n'importe
7
+ quel site tiers de lire des réponses authentifiées.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from wopt.checks.base import Check
13
+ from wopt.models import Finding, ScanContext, Severity
14
+
15
+
16
+ class CORSCheck(Check):
17
+ id = "cors.configuration"
18
+ name = "CORS Configuration"
19
+ category = "cors"
20
+
21
+ def run(self, ctx: ScanContext) -> list[Finding]:
22
+ findings: list[Finding] = []
23
+ headers_lower = {k.lower(): v for k, v in ctx.response_headers.items()}
24
+
25
+ acao = headers_lower.get("access-control-allow-origin")
26
+ acac = headers_lower.get("access-control-allow-credentials", "").lower() == "true"
27
+
28
+ if acao == "*" and acac:
29
+ findings.append(
30
+ Finding(
31
+ check_id=f"{self.id}.wildcard_with_credentials",
32
+ title="CORS critique : origine wildcard combinée aux credentials",
33
+ severity=Severity.CRITICAL,
34
+ category=self.category,
35
+ description=(
36
+ "Access-Control-Allow-Origin est réglé sur '*' alors que "
37
+ "Access-Control-Allow-Credentials est à 'true'. N'importe quel "
38
+ "site tiers peut lire des réponses authentifiées de l'utilisateur."
39
+ ),
40
+ recommendation=(
41
+ "Remplacer '*' par une liste blanche explicite d'origines de confiance, "
42
+ "ou désactiver les credentials si le wildcard est nécessaire."
43
+ ),
44
+ evidence=f"Access-Control-Allow-Origin: {acao}",
45
+ )
46
+ )
47
+ elif acao == "*":
48
+ findings.append(
49
+ Finding(
50
+ check_id=f"{self.id}.wildcard_origin",
51
+ title="CORS permissif : origine wildcard",
52
+ severity=Severity.LOW,
53
+ category=self.category,
54
+ description="Access-Control-Allow-Origin accepte toute origine ('*').",
55
+ recommendation="Restreindre aux origines réellement nécessaires si l'API contient des données sensibles.",
56
+ evidence=f"Access-Control-Allow-Origin: {acao}",
57
+ )
58
+ )
59
+
60
+ return findings
@@ -0,0 +1,60 @@
1
+ """
2
+ wopt.checks.exposure
3
+
4
+ Vérifie passivement si des chemins sensibles connus répondent
5
+ avec un statut 200 (fichier accessible). Aucune tentative de
6
+ lecture/exploitation du contenu au-delà de la détection : on
7
+ observe uniquement le code de statut HTTP renvoyé.
8
+
9
+ Ce check nécessite des requêtes HTTP additionnelles, effectuées
10
+ par le scanner (pas par ScanContext initial) via check.probe_paths().
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from wopt.checks.base import Check
16
+ from wopt.models import Finding, ScanContext, Severity
17
+
18
+ SENSITIVE_PATHS = {
19
+ "/.env": Severity.CRITICAL,
20
+ "/.git/HEAD": Severity.HIGH,
21
+ "/.git/config": Severity.HIGH,
22
+ "/wp-config.php.bak": Severity.CRITICAL,
23
+ "/config.php.bak": Severity.CRITICAL,
24
+ "/.DS_Store": Severity.LOW,
25
+ "/backup.zip": Severity.MEDIUM,
26
+ "/.aws/credentials": Severity.CRITICAL,
27
+ }
28
+
29
+
30
+ class ExposurePathsCheck(Check):
31
+ """Ce check est particulier : il a besoin de requêtes HTTP
32
+ supplémentaires (une par chemin testé). Le scanner l'invoque
33
+ via `probe_paths()` plutôt que `run()` classique -- voir
34
+ core/scanner.py pour l'intégration."""
35
+
36
+ id = "exposure.sensitive_paths"
37
+ name = "Sensitive Paths Exposure"
38
+ category = "exposure"
39
+
40
+ def paths_to_probe(self) -> dict[str, Severity]:
41
+ return SENSITIVE_PATHS
42
+
43
+ def build_finding(self, path: str, severity: Severity, status_code: int) -> Finding:
44
+ return Finding(
45
+ check_id=f"{self.id}.{path.strip('/').replace('/', '_').replace('.', '_')}",
46
+ title=f"Chemin sensible potentiellement exposé : {path}",
47
+ severity=severity,
48
+ category=self.category,
49
+ description=(
50
+ f"La requête vers '{path}' a retourné un statut {status_code}, "
51
+ "suggérant que ce fichier pourrait être accessible publiquement."
52
+ ),
53
+ recommendation=f"Vérifier manuellement '{path}' et bloquer son accès public si confirmé.",
54
+ evidence=f"HTTP {status_code}",
55
+ )
56
+
57
+ def run(self, ctx: ScanContext) -> list[Finding]:
58
+ # Ce check ne fait rien via run() seul -- voir probe_paths()
59
+ # dans le scanner qui gère les requêtes réseau réelles.
60
+ return []
wopt/checks/headers.py ADDED
@@ -0,0 +1,118 @@
1
+ """
2
+ wopt.checks.headers
3
+
4
+ Vérifie la présence et la qualité des en-têtes de sécurité HTTP.
5
+ C'est la faille la plus documentée dans les études sur le code
6
+ généré par IA : la quasi-totalité des applications vibe-codées
7
+ en sont dépourvues.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from wopt.checks.base import Check
13
+ from wopt.models import Finding, ScanContext, Severity
14
+
15
+ SECURITY_HEADERS = {
16
+ "strict-transport-security": {
17
+ "severity": Severity.HIGH,
18
+ "title": "HSTS absent",
19
+ "description": "Le header Strict-Transport-Security est absent : le navigateur "
20
+ "peut être redirigé vers une version HTTP non chiffrée du site.",
21
+ "recommendation": "Ajouter 'Strict-Transport-Security: max-age=31536000; includeSubDomains'.",
22
+ },
23
+ "content-security-policy": {
24
+ "severity": Severity.HIGH,
25
+ "title": "CSP absent",
26
+ "description": "Aucune Content-Security-Policy définie : la page est plus "
27
+ "vulnérable aux attaques XSS et à l'injection de contenu tiers.",
28
+ "recommendation": "Définir une Content-Security-Policy restrictive adaptée à l'application.",
29
+ },
30
+ "x-frame-options": {
31
+ "severity": Severity.MEDIUM,
32
+ "title": "X-Frame-Options absent",
33
+ "description": "Le site peut être intégré dans une iframe tierce, exposant "
34
+ "les utilisateurs à des attaques de clickjacking.",
35
+ "recommendation": "Ajouter 'X-Frame-Options: DENY' ou utiliser 'frame-ancestors' en CSP.",
36
+ },
37
+ "x-content-type-options": {
38
+ "severity": Severity.LOW,
39
+ "title": "X-Content-Type-Options absent",
40
+ "description": "Le navigateur peut tenter de deviner le type MIME des ressources, "
41
+ "ce qui ouvre la porte à des attaques par confusion de type.",
42
+ "recommendation": "Ajouter 'X-Content-Type-Options: nosniff'.",
43
+ },
44
+ "referrer-policy": {
45
+ "severity": Severity.LOW,
46
+ "title": "Referrer-Policy absent",
47
+ "description": "Sans Referrer-Policy, l'URL complète peut fuiter vers des sites tiers "
48
+ "via l'en-tête Referer.",
49
+ "recommendation": "Ajouter 'Referrer-Policy: strict-origin-when-cross-origin'.",
50
+ },
51
+ "permissions-policy": {
52
+ "severity": Severity.INFO,
53
+ "title": "Permissions-Policy absent",
54
+ "description": "Aucune restriction explicite sur les APIs navigateur sensibles "
55
+ "(caméra, micro, géolocalisation).",
56
+ "recommendation": "Ajouter 'Permissions-Policy' pour désactiver les APIs non utilisées.",
57
+ },
58
+ }
59
+
60
+
61
+ class SecurityHeadersCheck(Check):
62
+ id = "headers.security_headers"
63
+ name = "Security Headers"
64
+ category = "headers"
65
+
66
+ def run(self, ctx: ScanContext) -> list[Finding]:
67
+ findings: list[Finding] = []
68
+ headers_lower = {k.lower(): v for k, v in ctx.response_headers.items()}
69
+
70
+ for header_name, rule in SECURITY_HEADERS.items():
71
+ if header_name not in headers_lower:
72
+ findings.append(
73
+ Finding(
74
+ check_id=f"{self.id}.{header_name}_missing",
75
+ title=rule["title"],
76
+ severity=rule["severity"],
77
+ category=self.category,
78
+ description=rule["description"],
79
+ recommendation=rule["recommendation"],
80
+ )
81
+ )
82
+
83
+ # Vérification qualitative de la CSP, pas juste présence/absence
84
+ csp = headers_lower.get("content-security-policy")
85
+ if csp:
86
+ weak_tokens = [t for t in ("'unsafe-inline'", "'unsafe-eval'", "*") if t in csp]
87
+ if weak_tokens:
88
+ findings.append(
89
+ Finding(
90
+ check_id=f"{self.id}.csp_weak",
91
+ title="CSP affaiblie détectée",
92
+ severity=Severity.MEDIUM,
93
+ category=self.category,
94
+ description=(
95
+ "La Content-Security-Policy contient des directives permissives "
96
+ f"({', '.join(weak_tokens)}) qui réduisent sa protection contre le XSS."
97
+ ),
98
+ recommendation="Retirer 'unsafe-inline'/'unsafe-eval'/'*' et utiliser des nonces ou hashes.",
99
+ evidence=csp,
100
+ )
101
+ )
102
+
103
+ # Exposition de la stack technique
104
+ for leaky_header in ("server", "x-powered-by"):
105
+ if leaky_header in headers_lower and headers_lower[leaky_header]:
106
+ findings.append(
107
+ Finding(
108
+ check_id=f"{self.id}.stack_disclosure_{leaky_header}",
109
+ title=f"Divulgation de stack via '{leaky_header}'",
110
+ severity=Severity.INFO,
111
+ category=self.category,
112
+ description=f"Le header '{leaky_header}' révèle des informations sur la stack technique.",
113
+ recommendation=f"Masquer ou supprimer le header '{leaky_header}' côté serveur.",
114
+ evidence=headers_lower[leaky_header],
115
+ )
116
+ )
117
+
118
+ return findings
wopt/checks/tls.py ADDED
@@ -0,0 +1,100 @@
1
+ """
2
+ wopt.checks.tls
3
+
4
+ Vérifie la configuration TLS/SSL : version du protocole négociée
5
+ et date d'expiration du certificat. Les informations TLS sont
6
+ collectées en amont (core/http_client.py) et transmises via
7
+ ScanContext.tls_info pour éviter une connexion TLS redondante.
8
+
9
+ ScanContext.tls_info attendu (dict) :
10
+ {
11
+ "protocol_version": "TLSv1.3",
12
+ "not_after": "2026-09-01T00:00:00+00:00", # ISO 8601
13
+ "days_until_expiry": 60,
14
+ "cipher": "TLS_AES_256_GCM_SHA384",
15
+ }
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from wopt.checks.base import Check
21
+ from wopt.models import Finding, ScanContext, Severity
22
+
23
+ OUTDATED_PROTOCOLS = {"TLSv1", "TLSv1.0", "TLSv1.1", "SSLv2", "SSLv3"}
24
+
25
+
26
+ class TLSCheck(Check):
27
+ id = "tls.configuration"
28
+ name = "TLS Configuration"
29
+ category = "tls"
30
+
31
+ def run(self, ctx: ScanContext) -> list[Finding]:
32
+ findings: list[Finding] = []
33
+
34
+ if not ctx.url.startswith("https://"):
35
+ findings.append(
36
+ Finding(
37
+ check_id=f"{self.id}.no_https",
38
+ title="Le site n'est pas servi en HTTPS",
39
+ severity=Severity.CRITICAL,
40
+ category=self.category,
41
+ description="La cible a été scannée en HTTP simple, sans chiffrement TLS.",
42
+ recommendation="Migrer le site vers HTTPS avec un certificat valide.",
43
+ )
44
+ )
45
+ return findings
46
+
47
+ if ctx.tls_info is None:
48
+ findings.append(
49
+ Finding(
50
+ check_id=f"{self.id}.info_unavailable",
51
+ title="Impossible de récupérer les informations TLS",
52
+ severity=Severity.INFO,
53
+ category=self.category,
54
+ description="La négociation TLS a échoué ou n'a pas pu être analysée.",
55
+ recommendation="Vérifier manuellement la configuration TLS du serveur.",
56
+ )
57
+ )
58
+ return findings
59
+
60
+ protocol = ctx.tls_info.get("protocol_version")
61
+ if protocol in OUTDATED_PROTOCOLS:
62
+ findings.append(
63
+ Finding(
64
+ check_id=f"{self.id}.outdated_protocol",
65
+ title=f"Protocole TLS obsolète : {protocol}",
66
+ severity=Severity.HIGH,
67
+ category=self.category,
68
+ description=f"Le serveur accepte encore {protocol}, considéré comme non sûr.",
69
+ recommendation="Désactiver TLS < 1.2 et n'accepter que TLS 1.2 / 1.3.",
70
+ evidence=protocol,
71
+ )
72
+ )
73
+
74
+ days_left = ctx.tls_info.get("days_until_expiry")
75
+ if days_left is not None:
76
+ if days_left < 0:
77
+ findings.append(
78
+ Finding(
79
+ check_id=f"{self.id}.cert_expired",
80
+ title="Certificat TLS expiré",
81
+ severity=Severity.CRITICAL,
82
+ category=self.category,
83
+ description="Le certificat présenté par le serveur est expiré.",
84
+ recommendation="Renouveler le certificat TLS immédiatement.",
85
+ )
86
+ )
87
+ elif days_left < 14:
88
+ findings.append(
89
+ Finding(
90
+ check_id=f"{self.id}.cert_expiring_soon",
91
+ title=f"Certificat TLS expire dans {days_left} jours",
92
+ severity=Severity.MEDIUM,
93
+ category=self.category,
94
+ description="Le certificat arrive bientôt à expiration.",
95
+ recommendation="Planifier le renouvellement du certificat.",
96
+ evidence=str(days_left),
97
+ )
98
+ )
99
+
100
+ return findings
wopt/cli.py ADDED
@@ -0,0 +1,141 @@
1
+ """
2
+ wopt.cli
3
+
4
+ Point d'entrée en ligne de commande.
5
+
6
+ Exemples :
7
+ wopt scan exemple.com
8
+ wopt scan https://exemple.com --format json
9
+ wopt scan https://exemple.com --format html -o rapport.html
10
+ wopt scan https://exemple.com --ai-context
11
+ wopt scan https://exemple.com --no-probes
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ import typer
20
+ from rich.console import Console
21
+ from rich.table import Table
22
+
23
+ from wopt.core.http_client import FetchError
24
+ from wopt.core.scanner import scan
25
+ from wopt.models import Severity
26
+ from wopt.report.ai_context_output import to_ai_context
27
+ from wopt.report.html_output import to_html
28
+ from wopt.report.json_output import to_json
29
+
30
+ app = typer.Typer(
31
+ name="wopt",
32
+ help="Mini audit de sécurité web en ligne de commande.\n\n"
33
+ "Développé par Eloundou Nkolo Ryan — https://eloundounkolo.com",
34
+ add_completion=False,
35
+ )
36
+ console = Console()
37
+
38
+ SEVERITY_COLORS = {
39
+ Severity.CRITICAL: "bold red",
40
+ Severity.HIGH: "red",
41
+ Severity.MEDIUM: "yellow",
42
+ Severity.LOW: "blue",
43
+ Severity.INFO: "dim",
44
+ }
45
+
46
+ SCORE_COLORS = {"A": "green", "B": "green", "C": "yellow", "D": "yellow", "E": "red", "F": "bold red"}
47
+
48
+
49
+ @app.command()
50
+ def scan_command(
51
+ target: str = typer.Argument(..., help="URL ou domaine à scanner, ex: exemple.com"),
52
+ format: str = typer.Option("table", "--format", "-f", help="table | json | html"),
53
+ output: Optional[Path] = typer.Option(None, "--output", "-o", help="Fichier de sortie (json/html)"),
54
+ ai_context: bool = typer.Option(
55
+ False, "--ai-context", help="Génère un rapport markdown prêt à coller dans un agent IA"
56
+ ),
57
+ no_probes: bool = typer.Option(
58
+ False, "--no-probes", help="Désactive la vérification des chemins sensibles (scan plus léger)"
59
+ ),
60
+ timeout: float = typer.Option(10.0, "--timeout", help="Timeout HTTP en secondes"),
61
+ ):
62
+ """Lance un audit de sécurité non-intrusif sur TARGET."""
63
+ with console.status(f"[bold cyan]Scan de {target}...[/bold cyan]"):
64
+ try:
65
+ result = scan(target, timeout=timeout, include_probes=not no_probes)
66
+ except FetchError as exc:
67
+ console.print(f"[bold red]Erreur :[/bold red] {exc}")
68
+ raise typer.Exit(code=1)
69
+
70
+ if ai_context:
71
+ content = to_ai_context(result)
72
+ if output:
73
+ output.write_text(content, encoding="utf-8")
74
+ console.print(f"[green]Rapport IA écrit dans {output}[/green]")
75
+ else:
76
+ console.print(content)
77
+ return
78
+
79
+ if format == "json":
80
+ content = to_json(result)
81
+ if output:
82
+ output.write_text(content, encoding="utf-8")
83
+ console.print(f"[green]Rapport JSON écrit dans {output}[/green]")
84
+ else:
85
+ console.print(content)
86
+ return
87
+
88
+ if format == "html":
89
+ content = to_html(result)
90
+ out_path = output or Path("wopt-report.html")
91
+ out_path.write_text(content, encoding="utf-8")
92
+ console.print(f"[green]Rapport HTML écrit dans {out_path}[/green]")
93
+ return
94
+
95
+ _print_table(result)
96
+
97
+
98
+ def _print_table(result) -> None:
99
+ score_color = SCORE_COLORS.get(result.score, "white")
100
+ console.print()
101
+ console.print(f"[bold]Cible :[/bold] {result.target}")
102
+ console.print(f"[bold]Score :[/bold] [{score_color}]{result.score}[/{score_color}] ({result.score_value}/100)")
103
+ console.print(f"[dim]Scan effectué en {result.duration_ms:.0f} ms[/dim]")
104
+ console.print()
105
+
106
+ if not result.findings:
107
+ console.print("[green]✓ Aucun problème détecté.[/green]")
108
+ return
109
+
110
+ table = Table(show_lines=False)
111
+ table.add_column("Sévérité", no_wrap=True)
112
+ table.add_column("Catégorie", no_wrap=True)
113
+ table.add_column("Problème")
114
+ table.add_column("Recommandation")
115
+
116
+ severity_order = [
117
+ Severity.CRITICAL,
118
+ Severity.HIGH,
119
+ Severity.MEDIUM,
120
+ Severity.LOW,
121
+ Severity.INFO,
122
+ ]
123
+ for severity in severity_order:
124
+ for finding in result.findings_by_severity(severity):
125
+ color = SEVERITY_COLORS.get(severity, "white")
126
+ table.add_row(
127
+ f"[{color}]{severity.value.upper()}[/{color}]",
128
+ finding.category,
129
+ finding.title,
130
+ finding.recommendation,
131
+ )
132
+
133
+ console.print(table)
134
+
135
+
136
+ def main():
137
+ app()
138
+
139
+
140
+ if __name__ == "__main__":
141
+ main()
wopt/core/__init__.py ADDED
File without changes
@@ -0,0 +1,139 @@
1
+ """
2
+ wopt.core.http_client
3
+
4
+ Encapsule les requêtes HTTP nécessaires au scan : une requête
5
+ principale vers la cible, plus les informations TLS et cookies
6
+ extraites de la même connexion pour limiter le nombre de
7
+ requêtes effectuées.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import ssl
13
+ import time
14
+ from datetime import datetime, timezone
15
+
16
+ import httpx
17
+
18
+ from wopt.models import Cookie, ScanContext
19
+
20
+ DEFAULT_TIMEOUT = 10.0
21
+ DEFAULT_HEADERS = {
22
+ "User-Agent": "wopt/0.1 (+https://github.com/eloundou-nkolo/wopt) security-audit-tool",
23
+ }
24
+
25
+
26
+ class FetchError(Exception):
27
+ """Levée quand la cible ne peut pas être contactée."""
28
+
29
+
30
+ def _normalize_url(url: str) -> str:
31
+ if not url.startswith(("http://", "https://")):
32
+ url = f"https://{url}"
33
+ return url
34
+
35
+
36
+ def _extract_tls_info(url: str, timeout: float) -> dict | None:
37
+ """Récupère la version du protocole TLS et la date d'expiration
38
+ du certificat via une connexion SSL directe (indépendante de httpx,
39
+ qui n'expose pas facilement ces détails bas niveau)."""
40
+ if not url.startswith("https://"):
41
+ return None
42
+
43
+ hostname = url.split("://", 1)[1].split("/", 1)[0].split(":")[0]
44
+
45
+ try:
46
+ import socket
47
+
48
+ context = ssl.create_default_context()
49
+ with socket.create_connection((hostname, 443), timeout=timeout) as sock:
50
+ with context.wrap_socket(sock, server_hostname=hostname) as ssock:
51
+ cert = ssock.getpeercert()
52
+ protocol_version = ssock.version()
53
+ cipher = ssock.cipher()
54
+
55
+ not_after_str = cert.get("notAfter") if cert else None
56
+ days_until_expiry = None
57
+ if not_after_str:
58
+ not_after = datetime.strptime(not_after_str, "%b %d %H:%M:%S %Y %Z").replace(
59
+ tzinfo=timezone.utc
60
+ )
61
+ days_until_expiry = (not_after - datetime.now(timezone.utc)).days
62
+
63
+ return {
64
+ "protocol_version": protocol_version,
65
+ "not_after": not_after_str,
66
+ "days_until_expiry": days_until_expiry,
67
+ "cipher": cipher[0] if cipher else None,
68
+ }
69
+ except Exception:
70
+ # On ne bloque jamais le scan pour une erreur d'introspection TLS :
71
+ # le check TLS gérera lui-même le cas tls_info=None.
72
+ return None
73
+
74
+
75
+ def _parse_cookies(response: httpx.Response) -> list[Cookie]:
76
+ cookies = []
77
+ for header_value in response.headers.get_list("set-cookie"):
78
+ parts = [p.strip() for p in header_value.split(";")]
79
+ name = parts[0].split("=", 1)[0] if parts else "unknown"
80
+ lower_parts = [p.lower() for p in parts]
81
+
82
+ same_site = None
83
+ for p in parts:
84
+ if p.lower().startswith("samesite="):
85
+ same_site = p.split("=", 1)[1]
86
+
87
+ cookies.append(
88
+ Cookie(
89
+ name=name,
90
+ secure="secure" in lower_parts,
91
+ http_only="httponly" in lower_parts,
92
+ same_site=same_site,
93
+ )
94
+ )
95
+ return cookies
96
+
97
+
98
+ def fetch(url: str, timeout: float = DEFAULT_TIMEOUT) -> ScanContext:
99
+ """Effectue la requête principale vers la cible et construit
100
+ le ScanContext utilisé par tous les checks standards."""
101
+ url = _normalize_url(url)
102
+ start = time.perf_counter()
103
+
104
+ try:
105
+ with httpx.Client(
106
+ timeout=timeout,
107
+ follow_redirects=True,
108
+ headers=DEFAULT_HEADERS,
109
+ verify=True,
110
+ ) as client:
111
+ response = client.get(url)
112
+ except httpx.RequestError as exc:
113
+ raise FetchError(f"Impossible de contacter '{url}' : {exc}") from exc
114
+
115
+ elapsed_ms = (time.perf_counter() - start) * 1000
116
+ tls_info = _extract_tls_info(str(response.url), timeout)
117
+
118
+ return ScanContext(
119
+ url=url,
120
+ final_url=str(response.url),
121
+ status_code=response.status_code,
122
+ response_headers=dict(response.headers),
123
+ response_body=response.text,
124
+ cookies=_parse_cookies(response),
125
+ tls_info=tls_info,
126
+ elapsed_ms=elapsed_ms,
127
+ )
128
+
129
+
130
+ def probe_path(base_url: str, path: str, timeout: float = DEFAULT_TIMEOUT) -> int | None:
131
+ """Requête légère pour vérifier si un chemin spécifique répond.
132
+ Retourne le status_code, ou None si la requête échoue."""
133
+ target = base_url.rstrip("/") + path
134
+ try:
135
+ with httpx.Client(timeout=timeout, follow_redirects=False, headers=DEFAULT_HEADERS) as client:
136
+ response = client.get(target)
137
+ return response.status_code
138
+ except httpx.RequestError:
139
+ return None
wopt/core/scanner.py ADDED
@@ -0,0 +1,49 @@
1
+ """
2
+ wopt.core.scanner
3
+
4
+ Orchestrateur principal : récupère les données de la cible,
5
+ exécute tous les checks enregistrés, agrège les résultats en
6
+ un ScanResult prêt à être scoré et formaté.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import time
12
+
13
+ from wopt.checks import ALL_CHECKS, PROBE_CHECKS
14
+ from wopt.core.http_client import FetchError, fetch, probe_path
15
+ from wopt.models import Finding, ScanResult
16
+ from wopt.report.scoring import compute_score
17
+
18
+
19
+ def scan(url: str, timeout: float = 10.0, include_probes: bool = True) -> ScanResult:
20
+ """Lance un scan complet sur `url` et retourne un ScanResult.
21
+
22
+ Args:
23
+ url: URL ou nom de domaine de la cible.
24
+ timeout: timeout HTTP en secondes.
25
+ include_probes: si True, teste aussi les chemins sensibles
26
+ connus (requêtes HTTP additionnelles). Mettre à False
27
+ pour un scan plus rapide/moins bruyant côté serveur cible.
28
+ """
29
+ start = time.perf_counter()
30
+ findings: list[Finding] = []
31
+
32
+ ctx = fetch(url, timeout=timeout)
33
+
34
+ for check_cls in ALL_CHECKS:
35
+ check = check_cls()
36
+ findings.extend(check.run(ctx))
37
+
38
+ if include_probes:
39
+ for check_cls in PROBE_CHECKS:
40
+ check = check_cls()
41
+ for path, severity in check.paths_to_probe().items():
42
+ status = probe_path(ctx.final_url, path, timeout=timeout)
43
+ if status is not None and status < 400:
44
+ findings.append(check.build_finding(path, severity, status))
45
+
46
+ duration_ms = (time.perf_counter() - start) * 1000
47
+ result = ScanResult.build(target=ctx.final_url, findings=findings, duration_ms=duration_ms)
48
+ result.score, result.score_value = compute_score(findings)
49
+ return result
wopt/models.py ADDED
@@ -0,0 +1,103 @@
1
+ """
2
+ wopt.models
3
+
4
+ Structures de données partagées par tout le projet :
5
+ - Severity : niveau de gravité d'un résultat
6
+ - Finding : un résultat individuel produit par un check
7
+ - ScanContext : les données brutes collectées sur la cible, passées à chaque check
8
+ - ScanResult : le résultat final agrégé d'un scan complet
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass, field
14
+ from datetime import datetime, timezone
15
+ from enum import Enum
16
+
17
+
18
+ class Severity(str, Enum):
19
+ CRITICAL = "critical"
20
+ HIGH = "high"
21
+ MEDIUM = "medium"
22
+ LOW = "low"
23
+ INFO = "info"
24
+ PASS = "pass"
25
+
26
+ @property
27
+ def weight(self) -> int:
28
+ """Poids utilisé pour le calcul du score global."""
29
+ return {
30
+ Severity.CRITICAL: 25,
31
+ Severity.HIGH: 15,
32
+ Severity.MEDIUM: 8,
33
+ Severity.LOW: 3,
34
+ Severity.INFO: 0,
35
+ Severity.PASS: 0,
36
+ }[self]
37
+
38
+
39
+ @dataclass
40
+ class Finding:
41
+ """Un résultat individuel produit par un check de sécurité."""
42
+
43
+ check_id: str
44
+ title: str
45
+ severity: Severity
46
+ category: str
47
+ description: str
48
+ recommendation: str
49
+ evidence: str | None = None
50
+
51
+
52
+ @dataclass
53
+ class Cookie:
54
+ name: str
55
+ secure: bool
56
+ http_only: bool
57
+ same_site: str | None
58
+
59
+
60
+ @dataclass
61
+ class ScanContext:
62
+ """Toutes les données brutes collectées sur la cible.
63
+ Centralisé pour qu'un seul jeu de requêtes HTTP suffise
64
+ à alimenter tous les checks (pas de requêtes redondantes)."""
65
+
66
+ url: str
67
+ final_url: str
68
+ status_code: int
69
+ response_headers: dict[str, str]
70
+ response_body: str
71
+ cookies: list[Cookie] = field(default_factory=list)
72
+ tls_info: dict | None = None
73
+ elapsed_ms: float = 0.0
74
+
75
+
76
+ @dataclass
77
+ class ScanResult:
78
+ """Résultat final agrégé, prêt à être formaté (JSON/HTML/CLI)."""
79
+
80
+ target: str
81
+ scanned_at: datetime
82
+ findings: list[Finding]
83
+ score: str = "?" # lettre A-F
84
+ score_value: int = 0 # 0-100
85
+ duration_ms: float = 0.0
86
+
87
+ @classmethod
88
+ def build(cls, target: str, findings: list[Finding], duration_ms: float) -> "ScanResult":
89
+ return cls(
90
+ target=target,
91
+ scanned_at=datetime.now(timezone.utc),
92
+ findings=findings,
93
+ duration_ms=duration_ms,
94
+ )
95
+
96
+ def findings_by_severity(self, severity: Severity) -> list[Finding]:
97
+ return [f for f in self.findings if f.severity == severity]
98
+
99
+ def count_by_severity(self) -> dict[str, int]:
100
+ counts = {s.value: 0 for s in Severity}
101
+ for f in self.findings:
102
+ counts[f.severity.value] += 1
103
+ return counts
File without changes
@@ -0,0 +1,48 @@
1
+ """
2
+ wopt.report.ai_context_output
3
+
4
+ Formate un ScanResult en markdown structuré, optimisé pour être
5
+ collé directement dans un prompt d'agent IA (Claude Code, Cursor...)
6
+ afin qu'il corrige automatiquement les failles trouvées.
7
+ C'est le différenciateur "vibe coding" de wopt (v0.3).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from wopt.models import ScanResult, Severity
13
+
14
+ SEVERITY_ORDER = [
15
+ Severity.CRITICAL,
16
+ Severity.HIGH,
17
+ Severity.MEDIUM,
18
+ Severity.LOW,
19
+ Severity.INFO,
20
+ ]
21
+
22
+
23
+ def to_ai_context(result: ScanResult) -> str:
24
+ lines = [
25
+ f"# Audit de sécurité — {result.target}",
26
+ "",
27
+ f"Score global : {result.score} ({result.score_value}/100)",
28
+ "",
29
+ "Corrige les problèmes suivants, du plus critique au moins critique. "
30
+ "Pour chaque correctif, applique la recommandation indiquée.",
31
+ "",
32
+ ]
33
+
34
+ for severity in SEVERITY_ORDER:
35
+ items = result.findings_by_severity(severity)
36
+ if not items:
37
+ continue
38
+ lines.append(f"## {severity.value.upper()} ({len(items)})")
39
+ lines.append("")
40
+ for f in items:
41
+ lines.append(f"- **{f.title}**")
42
+ lines.append(f" - Problème : {f.description}")
43
+ lines.append(f" - Correctif : {f.recommendation}")
44
+ if f.evidence:
45
+ lines.append(f" - Preuve observée : `{f.evidence}`")
46
+ lines.append("")
47
+
48
+ return "\n".join(lines)
@@ -0,0 +1,28 @@
1
+ """
2
+ wopt.report.html_output
3
+
4
+ Rend le rapport HTML lisible via le template Jinja2.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ from jinja2 import Environment, FileSystemLoader, select_autoescape
12
+
13
+ from wopt.models import ScanResult
14
+
15
+ TEMPLATES_DIR = Path(__file__).parent / "templates"
16
+
17
+
18
+ def to_html(result: ScanResult) -> str:
19
+ env = Environment(
20
+ loader=FileSystemLoader(str(TEMPLATES_DIR)),
21
+ autoescape=select_autoescape(["html"]),
22
+ )
23
+ template = env.get_template("report.html.j2")
24
+ return template.render(
25
+ result=result,
26
+ findings=result.findings,
27
+ summary=result.count_by_severity(),
28
+ )
@@ -0,0 +1,39 @@
1
+ """
2
+ wopt.report.json_output
3
+
4
+ Sérialise un ScanResult en JSON, pensé pour être consommé par
5
+ un pipeline CI/CD ou un autre outil (parsing facile, structure stable).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+
12
+ from wopt.models import ScanResult
13
+
14
+
15
+ def to_dict(result: ScanResult) -> dict:
16
+ return {
17
+ "target": result.target,
18
+ "scanned_at": result.scanned_at.isoformat(),
19
+ "duration_ms": round(result.duration_ms, 2),
20
+ "score": result.score,
21
+ "score_value": result.score_value,
22
+ "summary": result.count_by_severity(),
23
+ "findings": [
24
+ {
25
+ "check_id": f.check_id,
26
+ "title": f.title,
27
+ "severity": f.severity.value,
28
+ "category": f.category,
29
+ "description": f.description,
30
+ "recommendation": f.recommendation,
31
+ "evidence": f.evidence,
32
+ }
33
+ for f in result.findings
34
+ ],
35
+ }
36
+
37
+
38
+ def to_json(result: ScanResult, indent: int = 2) -> str:
39
+ return json.dumps(to_dict(result), indent=indent, ensure_ascii=False)
wopt/report/scoring.py ADDED
@@ -0,0 +1,35 @@
1
+ """
2
+ wopt.report.scoring
3
+
4
+ Calcule un score de sécurité global (0-100, converti en lettre A-F)
5
+ à partir de la liste de findings, en pondérant par sévérité.
6
+ Inspiré de l'approche Mozilla Observatory : on part de 100 et on
7
+ soustrait des points selon la gravité de chaque finding.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from wopt.models import Finding
13
+
14
+ MAX_SCORE = 100
15
+
16
+
17
+ def compute_score(findings: list[Finding]) -> tuple[str, int]:
18
+ """Retourne (lettre, valeur_numerique)."""
19
+ penalty = sum(f.severity.weight for f in findings)
20
+ score_value = max(0, MAX_SCORE - penalty)
21
+ return _to_letter(score_value), score_value
22
+
23
+
24
+ def _to_letter(score_value: int) -> str:
25
+ if score_value >= 90:
26
+ return "A"
27
+ if score_value >= 75:
28
+ return "B"
29
+ if score_value >= 60:
30
+ return "C"
31
+ if score_value >= 40:
32
+ return "D"
33
+ if score_value >= 20:
34
+ return "E"
35
+ return "F"
@@ -0,0 +1,66 @@
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Rapport wopt — {{ result.target }}</title>
6
+ <style>
7
+ :root {
8
+ --bg: #0f1115; --card: #171a21; --text: #e8e8ea; --muted: #9aa0aa;
9
+ --critical: #e5484d; --high: #f76b15; --medium: #f5a623; --low: #4c9be8; --info: #6b7280; --pass: #3ecf8e;
10
+ }
11
+ * { box-sizing: border-box; }
12
+ body { background: var(--bg); color: var(--text); font-family: -apple-system, Segoe UI, Roboto, sans-serif; margin: 0; padding: 40px 20px; }
13
+ .container { max-width: 860px; margin: 0 auto; }
14
+ h1 { font-size: 1.4rem; word-break: break-all; }
15
+ .score-badge { display: inline-flex; align-items: center; justify-content: center; width: 72px; height: 72px; border-radius: 50%; font-size: 2rem; font-weight: 700; background: var(--card); border: 3px solid var(--pass); }
16
+ .meta { color: var(--muted); font-size: 0.9rem; margin-bottom: 30px; }
17
+ .summary { display: flex; gap: 10px; flex-wrap: wrap; margin: 20px 0 30px; }
18
+ .pill { padding: 4px 12px; border-radius: 999px; font-size: 0.8rem; font-weight: 600; }
19
+ .finding { background: var(--card); border-radius: 10px; padding: 16px 20px; margin-bottom: 12px; border-left: 4px solid var(--muted); }
20
+ .finding.critical { border-color: var(--critical); }
21
+ .finding.high { border-color: var(--high); }
22
+ .finding.medium { border-color: var(--medium); }
23
+ .finding.low { border-color: var(--low); }
24
+ .finding.info { border-color: var(--info); }
25
+ .finding h3 { margin: 0 0 8px; font-size: 1rem; }
26
+ .finding p { margin: 4px 0; color: var(--muted); font-size: 0.9rem; }
27
+ .finding .reco { color: var(--text); }
28
+ .sev-tag { display: inline-block; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; padding: 2px 8px; border-radius: 4px; margin-bottom: 8px; }
29
+ .sev-tag.critical { background: var(--critical); }
30
+ .sev-tag.high { background: var(--high); }
31
+ .sev-tag.medium { background: var(--medium); color: #1a1a1a; }
32
+ .sev-tag.low { background: var(--low); }
33
+ .sev-tag.info { background: var(--info); }
34
+ footer { margin-top: 40px; color: var(--muted); font-size: 0.8rem; text-align: center; }
35
+ </style>
36
+ </head>
37
+ <body>
38
+ <div class="container">
39
+ <div class="score-badge">{{ result.score }}</div>
40
+ <h1>Rapport de sécurité — {{ result.target }}</h1>
41
+ <div class="meta">Scanné le {{ result.scanned_at.strftime("%d/%m/%Y à %H:%M UTC") }} · {{ "%.0f"|format(result.duration_ms) }} ms · Score {{ result.score_value }}/100</div>
42
+
43
+ <div class="summary">
44
+ {% for sev, count in summary.items() if count > 0 %}
45
+ <span class="pill {{ sev }}">{{ sev }} : {{ count }}</span>
46
+ {% endfor %}
47
+ </div>
48
+
49
+ {% for finding in findings %}
50
+ <div class="finding {{ finding.severity.value }}">
51
+ <span class="sev-tag {{ finding.severity.value }}">{{ finding.severity.value }}</span>
52
+ <h3>{{ finding.title }}</h3>
53
+ <p>{{ finding.description }}</p>
54
+ <p class="reco">→ {{ finding.recommendation }}</p>
55
+ {% if finding.evidence %}<p><code>{{ finding.evidence }}</code></p>{% endif %}
56
+ </div>
57
+ {% else %}
58
+ <p>Aucun problème détecté.</p>
59
+ {% endfor %}
60
+
61
+ <footer>Généré par <a href="https://github.com/eloundou-nkolo/wopt">wopt</a> —
62
+ audit de sécurité web open-source, par
63
+ <a href="https://eloundounkolo.com">Eloundou Nkolo Ryan</a></footer>
64
+ </div>
65
+ </body>
66
+ </html>
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: wopt
3
+ Version: 0.1.0
4
+ Summary: Mini audit de sécurité web en ligne de commande : headers, TLS, cookies, CORS, fichiers exposés.
5
+ Project-URL: Homepage, https://github.com/eloundou-nkolo/wopt
6
+ Project-URL: Repository, https://github.com/eloundou-nkolo/wopt
7
+ Project-URL: Issues, https://github.com/eloundou-nkolo/wopt/issues
8
+ Project-URL: Portfolio, https://eloundounkolo.com
9
+ Author-email: Eloundou Nkolo Ryan <ryan@eloundounkolo.com>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: audit,cli,cors,http-headers,security,tls,web
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Security
22
+ Classifier: Topic :: Software Development :: Quality Assurance
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: cryptography
25
+ Requires-Dist: httpx
26
+ Requires-Dist: jinja2
27
+ Requires-Dist: rich
28
+ Requires-Dist: typer
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest; extra == 'dev'
31
+ Requires-Dist: pytest-httpx; extra == 'dev'
32
+ Requires-Dist: ruff; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # wopt
36
+
37
+ test audit de sécurité web en ligne de commande — non-intrusif, sans configuration, sans infrastructure.
38
+
39
+ `wopt` scanne un site web et vérifie les points de sécurité les plus fréquemment négligés : en-têtes HTTP, configuration TLS, cookies, CORS, et exposition de fichiers sensibles. Conçu pour s'intégrer facilement à un pipeline CI/CD.
40
+ ## Auteurs
41
+
42
+ Développé par [Eloundou Nkolo Ryan](https://eloundounkolo.com) — développeur web & intégration de solutions numériques.
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ pip install wopt
48
+ ```
49
+
50
+ Ou en local depuis le code source :
51
+
52
+ ```bash
53
+ git clone https://github.com/eloundou-nkolo/wopt.git
54
+ cd wopt
55
+ pip install -r requirements.txt
56
+ pip install -e .
57
+ ```
58
+
59
+ ## Utilisation
60
+
61
+ ```bash
62
+ # Scan simple, affichage table dans le terminal
63
+ wopt exemple.com
64
+
65
+ # Export JSON (pour CI/CD)
66
+ wopt exemple.com --format json --output rapport.json
67
+
68
+ # Export HTML lisible
69
+ wopt exemple.com --format html --output rapport.html
70
+
71
+ # Rapport formaté pour être collé dans un agent IA (Claude Code, Cursor...)
72
+ wopt exemple.com --ai-context
73
+
74
+ # Scan plus léger, sans vérification des chemins sensibles
75
+ wopt exemple.com --no-probes
76
+ ```
77
+
78
+ ## Ce que wopt vérifie
79
+
80
+ | Catégorie | Vérifications |
81
+ |---|---|
82
+ | **Headers** | CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, divulgation de stack |
83
+ | **TLS** | Version du protocole, expiration du certificat, redirection HTTPS |
84
+ | **Cookies** | Flags Secure, HttpOnly, SameSite |
85
+ | **CORS** | Combinaison dangereuse wildcard + credentials |
86
+ | **Exposition** | Fichiers sensibles accessibles (`.env`, `.git/HEAD`, backups...) |
87
+
88
+ ## Philosophie
89
+
90
+ - **100% passif et non-intrusif** : aucun scan de ports, aucune tentative d'exploitation, aucun brute-force. Uniquement des requêtes HTTP GET standards.
91
+ - **Zéro infrastructure** : tourne entièrement en local, aucune donnée envoyée à un serveur tiers.
92
+ - **Pensé pour l'ère du vibe coding** : conçu pour combler les lacunes de sécurité fréquentes dans le code généré par IA (absence de headers de sécurité, CSRF, CORS mal configuré).
93
+
94
+ ## Score
95
+
96
+ Chaque scan produit un score de A à F, calculé par pondération de sévérité (inspiré de l'approche Mozilla Observatory).
97
+
98
+ ## Développement
99
+
100
+ ```bash
101
+ pip install -r requirements-dev.txt
102
+ pytest
103
+ ruff check .
104
+ ```
105
+
106
+ ## Avertissement
107
+
108
+ `wopt` est un outil d'audit passif destiné à un usage sur des sites que vous possédez ou êtes autorisé à tester. L'auteur décline toute responsabilité en cas d'usage non autorisé.
109
+
110
+ ## Licence
111
+
112
+ MIT
@@ -0,0 +1,24 @@
1
+ wopt/__init__.py,sha256=tU-pobM-yWKZhtZ3PqBcDTroR7Js1RvycpGKqKulTag,160
2
+ wopt/cli.py,sha256=SC9qr-QxJdL1mRmZg01QlmuKqnGuhvHYGrHLeV71mjc,4447
3
+ wopt/models.py,sha256=vmtsXjaOqp63A0c062-Brxc_lRrHvdhxk-B_Gx8YkHs,2662
4
+ wopt/checks/__init__.py,sha256=BVaYpaXaw7ihe0Lh89wY-psJD4wFzbWSQyL-MW9TQEI,871
5
+ wopt/checks/base.py,sha256=2oAwoRH4U24dsV7UtOaFaCtSt1JqdeAzVeYCkvtANHQ,878
6
+ wopt/checks/cookies.py,sha256=oueyKso05z7gFtq8_kblWJZDUhkjOTbY6dPpO0qHo_o,2457
7
+ wopt/checks/cors.py,sha256=4cLD2y7paxnd8YbgTaH_QOZ6DvHt2_0ngG-aGD28cYw,2477
8
+ wopt/checks/exposure.py,sha256=-OIDSqA4w4vQYL8thOc8mdyZRpPbwGrdNYOVG8RMsqs,2234
9
+ wopt/checks/headers.py,sha256=1onvnX0zOKYhUR7DVPOVYKJ-EOZnEGt9OijPAFbdQhI,5245
10
+ wopt/checks/tls.py,sha256=MUAim5Xj56hujVIm0snpu-THBlM2v4SROXEzF5INj-I,3904
11
+ wopt/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ wopt/core/http_client.py,sha256=xFSXXUeN6Y1U1-H2MFemQf-oUcLmwoEkwi6SZr19KeA,4552
13
+ wopt/core/scanner.py,sha256=AvyPkyKRv_jkukaOwW-K5evgpPij1kwixmIHMYxldM0,1740
14
+ wopt/report/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ wopt/report/ai_context_output.py,sha256=-JYhMLS8O3Q-XPnGIqlr7M8HfBTIvqX9VJ1CQlWlNVc,1446
16
+ wopt/report/html_output.py,sha256=W3LZVxQ8jWTn_km3LEG9XNVWp2yuH5-eJtkuCJwFcTo,663
17
+ wopt/report/json_output.py,sha256=Rd0tolmmUch8oOi3hEyXklwDZ-kKqdZrYcxCIpoXiGM,1107
18
+ wopt/report/scoring.py,sha256=sXIrwRVYYe_oSzqGXbR9PV9Hi7mh5fybhnY1gDWJlSw,919
19
+ wopt/report/templates/report.html.j2,sha256=MINFwZYM1SlXtJ9hXEAHFFt6JqIw7ZB5IUteoLQf3Ww,3310
20
+ wopt-0.1.0.dist-info/METADATA,sha256=RxC7k-Jh2Vcz6EW2of2tmOdaQxgJxr6m9dMJHfmeV_k,3883
21
+ wopt-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
22
+ wopt-0.1.0.dist-info/entry_points.txt,sha256=MM9fObXlgpeDJxQUFD3B0QBj0keTqD9P5tumYsO4Eyc,39
23
+ wopt-0.1.0.dist-info/licenses/LICENSE,sha256=jYiTBzP0DvZuaaeiCduAocg5-hp0lILAcwrTTtb0a4k,1095
24
+ wopt-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ wopt = wopt.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 eloundou nkolo ryan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.