shorless 0.8.6__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.
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shorless
|
|
3
|
+
Version: 0.8.6
|
|
4
|
+
Summary: Scanner DevSecOps Post-Quantique (PQC) & Générateur CBOM CycloneDX 1.6
|
|
5
|
+
Author-email: Shorless Security <contact@shorless.io>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://shorless.io
|
|
8
|
+
Project-URL: Documentation, https://shorless.io/scanner-documentation.html
|
|
9
|
+
Project-URL: Repository, https://github.com/Shorless/scanner
|
|
10
|
+
Keywords: pqc,post-quantum,cbom,cyclonedx,devsecops,cryptography,ml-kem
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: typer>=0.9.0
|
|
14
|
+
Requires-Dist: rich>=13.0.0
|
|
15
|
+
|
|
16
|
+
# ⚡ Shorless — Post-Quantum (PQC) DevSecOps Scanner & CycloneDX 1.6 CBOM Generator
|
|
17
|
+
|
|
18
|
+
[](https://pypi.org/project/shorless/)
|
|
19
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
20
|
+
[](https://cyclonedx.org/capabilities/cbom/)
|
|
21
|
+
|
|
22
|
+
**Shorless** (`shorless`) est un scanner statique (AST), d'infrastructure (Nginx, Docker) et réseau (`--check-tls`) Open Source conçu pour aider les équipes DevSecOps de PME et d'ETI à cartographier leur dette cryptographique (RSA, ECDSA, SHA-1, OpenSSL < 3.2, TLS non hybridé) et à préparer leur transition vers la cryptographie post-quantique (**ANSSI** / **NIST FIPS 203 ML-KEM** & **FIPS 204 ML-DSA**).
|
|
23
|
+
|
|
24
|
+
🔗 **Site officiel & Sandbox interactif :** [https://shorless.io](https://shorless.io)
|
|
25
|
+
📘 **Documentation complète du scanner :** [https://shorless.io/scanner-documentation.html](https://shorless.io/scanner-documentation.html)
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 🆕 Nouveautés de la version v0.8.6 (Support Dockerfile & Inspection TLS Live (--check-tls))
|
|
30
|
+
|
|
31
|
+
- Ajout du scan des Dockerfiles et détection OpenSSL < 3.2
|
|
32
|
+
- Ajout de l'option --check-tls pour auditer un serveur TLS en direct
|
|
33
|
+
- Export unifié Code + Infra + Réseau dans le CBOM CycloneDX 1.6
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 🚀 Installation & Utilisation Rapide (PyPI / pipx)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# 1. Audit complet Code (AST) + Infra (Nginx / Dockerfile) + Export CBOM CycloneDX 1.6
|
|
41
|
+
pipx run shorless . --cbom cbom.json
|
|
42
|
+
|
|
43
|
+
# 2. Audit incluant l'inspection en direct d'un serveur TLS en production
|
|
44
|
+
shorless . --check-tls https://shorless.io --cbom cbom.json
|
|
45
|
+
|
|
46
|
+
# 3. Mode bloqueur CI/CD (renvoie exit code 1 si un risque critique face à Shor est trouvé)
|
|
47
|
+
shorless . --cbom cbom.json --ci
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 📄 Licence
|
|
53
|
+
|
|
54
|
+
Distribué sous licence **Apache 2.0**. © 2026 Shorless Security.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
shorless_scanner.py,sha256=mmF820baYBl3BPsNqiT_tQRpPu6ZuuNDlcleDjqAuqA,17125
|
|
2
|
+
shorless-0.8.6.dist-info/METADATA,sha256=KQdF-p6iwhfSksR1iVmCfIDc9WCADaWcZoDEu2KZNV0,2572
|
|
3
|
+
shorless-0.8.6.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
4
|
+
shorless-0.8.6.dist-info/entry_points.txt,sha256=HGA3ylT4RGWIXTSXqIQAL4LDLdvapcGqp8ADje5fvy8,50
|
|
5
|
+
shorless-0.8.6.dist-info/top_level.txt,sha256=m-MHzBKn-PKhGZYrg4BMNRyLjBWIg_vUP2augX8bxbk,17
|
|
6
|
+
shorless-0.8.6.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
shorless_scanner
|
shorless_scanner.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
================================================================================
|
|
5
|
+
SHORLESS PQC & CBOM SCANNER (shorless_scanner.py)
|
|
6
|
+
================================================================================
|
|
7
|
+
Scanner DevSecOps Open Source (Apache 2.0) de préparation Post-Quantique :
|
|
8
|
+
1. Analyse statique AST Python (RSA, ECDSA P-256, SHA-1, MD5).
|
|
9
|
+
2. Inspection des configurations TLS Nginx (.conf) et courbes hybrides ML-KEM.
|
|
10
|
+
3. Inspection des Dockerfiles (Dockerfile, Dockerfile.*, *.dockerfile) et versions OpenSSL.
|
|
11
|
+
4. Inspection en direct des terminaisons TLS distantes (--check-tls https://...).
|
|
12
|
+
5. Génération native de l'inventaire CBOM au standard OWASP CycloneDX 1.6 (cbom.json).
|
|
13
|
+
================================================================================
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import ast
|
|
17
|
+
import json
|
|
18
|
+
import re
|
|
19
|
+
import shutil
|
|
20
|
+
import socket
|
|
21
|
+
import ssl
|
|
22
|
+
import subprocess
|
|
23
|
+
import uuid
|
|
24
|
+
from datetime import datetime, timezone
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from urllib.parse import urlparse
|
|
27
|
+
import typer
|
|
28
|
+
from rich.console import Console
|
|
29
|
+
from rich.table import Table
|
|
30
|
+
from rich.panel import Panel
|
|
31
|
+
|
|
32
|
+
app = typer.Typer(help="Scanner DevSecOps de préparation Post-Quantique (PQC) & Générateur CBOM CycloneDX 1.6")
|
|
33
|
+
console = Console()
|
|
34
|
+
|
|
35
|
+
# Base de connaissances PQC (Règles NIST FIPS 203/204 & ANSSI)
|
|
36
|
+
PQC_RULES = {
|
|
37
|
+
"generate_private_key": {
|
|
38
|
+
"algo": "RSA / ECC (Asymétrique)",
|
|
39
|
+
"risk": "CRITIQUE",
|
|
40
|
+
"reason": "Vulnérable à l'algorithme de Shor (Harvest Now, Decrypt Later).",
|
|
41
|
+
"remediation": "Migrer vers une approche hybride (ex: X25519 + ML-KEM-768) ou signatures ML-DSA."
|
|
42
|
+
},
|
|
43
|
+
"SECP256R1": {
|
|
44
|
+
"algo": "ECDSA / ECDH (P-256)",
|
|
45
|
+
"risk": "CRITIQUE",
|
|
46
|
+
"reason": "Courbe elliptique classique cassable par un ordinateur quantique cryptographiquement pertinent.",
|
|
47
|
+
"remediation": "Utiliser liboqs / OpenSSL 3.2+ avec un provider OQS pour hybrider la courbe."
|
|
48
|
+
},
|
|
49
|
+
"SECP384R1": {
|
|
50
|
+
"algo": "ECDSA / ECDH (P-384)",
|
|
51
|
+
"risk": "CRITIQUE",
|
|
52
|
+
"reason": "Courbe elliptique classique vulnérable à l'algorithme de Shor.",
|
|
53
|
+
"remediation": "Hybrider l'échange de clés avec ML-KEM-768 ou ML-KEM-1024 (NIST FIPS 203)."
|
|
54
|
+
},
|
|
55
|
+
"SHA1": {
|
|
56
|
+
"algo": "SHA-1 (Hachage)",
|
|
57
|
+
"risk": "ÉLEVÉ",
|
|
58
|
+
"reason": "Obsolète (collisions classiques + affaiblissement par l'algorithme de Grover).",
|
|
59
|
+
"remediation": "Remplacer par SHA-256, SHA-384 ou SHA-3."
|
|
60
|
+
},
|
|
61
|
+
"MD5": {
|
|
62
|
+
"algo": "MD5 (Hachage)",
|
|
63
|
+
"risk": "ÉLEVÉ",
|
|
64
|
+
"reason": "Totalement compromis.",
|
|
65
|
+
"remediation": "Remplacer par SHA-256 ou SHA-3."
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class PythonCryptoVisitor(ast.NodeVisitor):
|
|
71
|
+
"""Analyse l'arbre syntaxique (AST) du code Python pour détecter les appels cryptographiques."""
|
|
72
|
+
def __init__(self, filepath: str):
|
|
73
|
+
self.filepath = filepath
|
|
74
|
+
self.findings = []
|
|
75
|
+
|
|
76
|
+
def visit_Call(self, node: ast.Call):
|
|
77
|
+
func_name = ""
|
|
78
|
+
if isinstance(node.func, ast.Attribute):
|
|
79
|
+
func_name = node.func.attr
|
|
80
|
+
elif isinstance(node.func, ast.Name):
|
|
81
|
+
func_name = node.func.id
|
|
82
|
+
|
|
83
|
+
if func_name in PQC_RULES:
|
|
84
|
+
rule = PQC_RULES[func_name]
|
|
85
|
+
self.findings.append({
|
|
86
|
+
"file": self.filepath,
|
|
87
|
+
"line": node.lineno,
|
|
88
|
+
"type": "Code Python (AST)",
|
|
89
|
+
"detected": func_name,
|
|
90
|
+
"algo": rule["algo"],
|
|
91
|
+
"risk": rule["risk"],
|
|
92
|
+
"remediation": rule["remediation"]
|
|
93
|
+
})
|
|
94
|
+
self.generic_visit(node)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def scan_nginx_config(filepath: Path) -> list:
|
|
98
|
+
"""Scanne les fichiers de configuration Nginx pour vérifier la présence de TLS hybride PQC."""
|
|
99
|
+
findings = []
|
|
100
|
+
content = filepath.read_text(encoding="utf-8", errors="ignore")
|
|
101
|
+
lines = content.splitlines()
|
|
102
|
+
|
|
103
|
+
has_ssl = False
|
|
104
|
+
has_pqc_curves = False
|
|
105
|
+
|
|
106
|
+
for i, line in enumerate(lines, start=1):
|
|
107
|
+
clean_line = line.strip()
|
|
108
|
+
if clean_line.startswith("#"):
|
|
109
|
+
continue
|
|
110
|
+
if "ssl_protocols" in clean_line:
|
|
111
|
+
has_ssl = True
|
|
112
|
+
if "TLSv1.3" not in clean_line:
|
|
113
|
+
findings.append({
|
|
114
|
+
"file": str(filepath),
|
|
115
|
+
"line": i,
|
|
116
|
+
"type": "Infra (Nginx)",
|
|
117
|
+
"detected": clean_line,
|
|
118
|
+
"algo": "TLS < 1.3",
|
|
119
|
+
"risk": "CRITIQUE",
|
|
120
|
+
"remediation": "Activer TLSv1.3 (prérequis obligatoire pour l'échange de clés post-quantique ML-KEM)."
|
|
121
|
+
})
|
|
122
|
+
if "ssl_ecdh_curve" in clean_line:
|
|
123
|
+
if "MLKEM" in clean_line.upper() or "KYBER" in clean_line.upper():
|
|
124
|
+
has_pqc_curves = True
|
|
125
|
+
|
|
126
|
+
if has_ssl and not has_pqc_curves:
|
|
127
|
+
findings.append({
|
|
128
|
+
"file": str(filepath),
|
|
129
|
+
"line": 1,
|
|
130
|
+
"type": "Infra (Nginx)",
|
|
131
|
+
"detected": "Absence de courbe hybride PQC",
|
|
132
|
+
"algo": "Échange de clés ECDHE classique",
|
|
133
|
+
"risk": "MOYEN",
|
|
134
|
+
"remediation": "Ajouter 'ssl_ecdh_curve X25519MLKEM768:X25519;' (Supporté dès OpenSSL 3.2+ / Nginx récent)."
|
|
135
|
+
})
|
|
136
|
+
return findings
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def scan_dockerfile(filepath: Path) -> list:
|
|
140
|
+
"""Scanne les Dockerfiles pour détecter les images ou versions d'OpenSSL incompatibles avec la PQC."""
|
|
141
|
+
findings = []
|
|
142
|
+
content = filepath.read_text(encoding="utf-8", errors="ignore")
|
|
143
|
+
lines = content.splitlines()
|
|
144
|
+
|
|
145
|
+
legacy_base_patterns = (
|
|
146
|
+
"buster", "stretch", "bullseye", "ubuntu:18.04", "ubuntu:20.04",
|
|
147
|
+
"alpine:3.12", "alpine:3.13", "alpine:3.14", "alpine:3.15", "alpine:3.16", "alpine:3.17", "alpine:3.18"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
for i, line in enumerate(lines, start=1):
|
|
151
|
+
clean_line = line.strip()
|
|
152
|
+
if not clean_line or clean_line.startswith("#"):
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
lower_line = clean_line.lower()
|
|
156
|
+
|
|
157
|
+
# 1. Vérification de l'image de base (FROM)
|
|
158
|
+
if lower_line.startswith("from "):
|
|
159
|
+
if any(pat in lower_line for pat in legacy_base_patterns):
|
|
160
|
+
findings.append({
|
|
161
|
+
"file": str(filepath),
|
|
162
|
+
"line": i,
|
|
163
|
+
"type": "Conteneur (Docker)",
|
|
164
|
+
"detected": clean_line,
|
|
165
|
+
"algo": "Image OS Legacy (OpenSSL < 3.2)",
|
|
166
|
+
"risk": "ÉLEVÉ",
|
|
167
|
+
"remediation": "Migrer vers une image de base récente (ex: debian:trixie, ubuntu:24.04, alpine:3.20+) embarquant OpenSSL 3.2+."
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
# 2. Vérification d'installation explicite d'OpenSSL obsolète (1.1.x, 1.0.x, 3.0.x)
|
|
171
|
+
if "openssl" in lower_line and any(ver in lower_line for ver in ("1.1.", "1.0.", "3.0.", "libssl1.1")):
|
|
172
|
+
findings.append({
|
|
173
|
+
"file": str(filepath),
|
|
174
|
+
"line": i,
|
|
175
|
+
"type": "Conteneur (Docker)",
|
|
176
|
+
"detected": clean_line,
|
|
177
|
+
"algo": "OpenSSL Obsolète (Incompatible PQC)",
|
|
178
|
+
"risk": "CRITIQUE",
|
|
179
|
+
"remediation": "Mettre à niveau vers OpenSSL 3.2+ (ou 3.5+) avec oqs-provider pour supporter ML-KEM (FIPS 203) et ML-DSA (FIPS 204)."
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
# 3. Détection de génération de clés RSA/ECDSA en ligne de commande dans le Dockerfile
|
|
183
|
+
if "openssl genrsa" in lower_line or "ssh-keygen -t rsa" in lower_line or "ecparam" in lower_line:
|
|
184
|
+
findings.append({
|
|
185
|
+
"file": str(filepath),
|
|
186
|
+
"line": i,
|
|
187
|
+
"type": "Conteneur (Docker)",
|
|
188
|
+
"detected": clean_line,
|
|
189
|
+
"algo": "Génération de clé RSA/ECC dans l'image",
|
|
190
|
+
"risk": "CRITIQUE",
|
|
191
|
+
"remediation": "Ne pas générer de clés asymétriques classiques dans le build Docker ; utiliser des certificats hybrides injectés au runtime."
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
return findings
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def scan_live_tls_endpoint(target_url: str) -> list:
|
|
198
|
+
"""Inspecte un endpoint TLS en direct pour vérifier TLS 1.3, le certificat et l'hybridation PQC."""
|
|
199
|
+
findings = []
|
|
200
|
+
|
|
201
|
+
if "://" not in target_url:
|
|
202
|
+
target_url = f"https://{target_url}"
|
|
203
|
+
parsed = urlparse(target_url)
|
|
204
|
+
hostname = parsed.hostname or target_url
|
|
205
|
+
port = parsed.port or 443
|
|
206
|
+
endpoint_label = f"{hostname}:{port}"
|
|
207
|
+
|
|
208
|
+
console.print(f"[cyan]🌐 Inspection TLS en direct sur :[/cyan] [bold white]{endpoint_label}[/bold white]")
|
|
209
|
+
|
|
210
|
+
tls_version = None
|
|
211
|
+
cipher_name = None
|
|
212
|
+
try:
|
|
213
|
+
ctx = ssl.create_default_context()
|
|
214
|
+
with socket.create_connection((hostname, port), timeout=6) as sock:
|
|
215
|
+
with ctx.wrap_socket(sock, server_hostname=hostname) as ssock:
|
|
216
|
+
tls_version = ssock.version()
|
|
217
|
+
cipher_info = ssock.cipher()
|
|
218
|
+
cipher_name = cipher_info[0] if cipher_info else "Inconnu"
|
|
219
|
+
except Exception as e:
|
|
220
|
+
findings.append({
|
|
221
|
+
"file": endpoint_label,
|
|
222
|
+
"line": port,
|
|
223
|
+
"type": "Réseau (TLS Live)",
|
|
224
|
+
"detected": f"Erreur de connexion : {e}",
|
|
225
|
+
"algo": "Endpoint TLS Injoignable / Invalide",
|
|
226
|
+
"risk": "ÉLEVÉ",
|
|
227
|
+
"remediation": "Vérifier l'accessibilité réseau, le pare-feu et la validité de la chaîne de certificats X.509."
|
|
228
|
+
})
|
|
229
|
+
return findings
|
|
230
|
+
|
|
231
|
+
if tls_version != "TLSv1.3":
|
|
232
|
+
findings.append({
|
|
233
|
+
"file": endpoint_label,
|
|
234
|
+
"line": port,
|
|
235
|
+
"type": "Réseau (TLS Live)",
|
|
236
|
+
"detected": f"Protocole négocié : {tls_version} ({cipher_name})",
|
|
237
|
+
"algo": f"{tls_version} (< TLS 1.3)",
|
|
238
|
+
"risk": "CRITIQUE",
|
|
239
|
+
"remediation": "Activer TLSv1.3 sur le reverse-proxy / Load Balancer (requis pour l'échange de clés hybride ML-KEM)."
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
openssl_bin = shutil.which("openssl")
|
|
243
|
+
negotiated_group = ""
|
|
244
|
+
cert_sig_algo = ""
|
|
245
|
+
|
|
246
|
+
if openssl_bin:
|
|
247
|
+
try:
|
|
248
|
+
proc = subprocess.run(
|
|
249
|
+
[openssl_bin, "s_client", "-connect", endpoint_label, "-servername", hostname, "-brief"],
|
|
250
|
+
input="",
|
|
251
|
+
capture_output=True,
|
|
252
|
+
text=True,
|
|
253
|
+
encoding="utf-8",
|
|
254
|
+
errors="ignore",
|
|
255
|
+
timeout=6
|
|
256
|
+
)
|
|
257
|
+
output = (proc.stdout or "") + "\n" + (proc.stderr or "")
|
|
258
|
+
for line in output.splitlines():
|
|
259
|
+
low = line.lower()
|
|
260
|
+
if "Server Temp Key" in line or "Peer Temp Key" in line or "Negotiated TLS1.3 group" in line:
|
|
261
|
+
negotiated_group = line.strip()
|
|
262
|
+
elif "peer signing digest" in low or "signature type" in low:
|
|
263
|
+
cert_sig_algo = line.strip()
|
|
264
|
+
except Exception:
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
is_pqc_hybrid = any(k in negotiated_group.upper() for k in ("MLKEM", "KYBER"))
|
|
268
|
+
if not is_pqc_hybrid:
|
|
269
|
+
detected_kem = negotiated_group if negotiated_group else f"Handshake {tls_version} ({cipher_name}) sans KEM hybride"
|
|
270
|
+
findings.append({
|
|
271
|
+
"file": endpoint_label,
|
|
272
|
+
"line": port,
|
|
273
|
+
"type": "Réseau (TLS Live)",
|
|
274
|
+
"detected": detected_kem,
|
|
275
|
+
"algo": "Échange de clés TLS non hybridé (ECDHE classique)",
|
|
276
|
+
"risk": "MOYEN",
|
|
277
|
+
"remediation": "Activer la courbe hybride 'X25519MLKEM768:X25519' (OpenSSL 3.2+ / Nginx / Cloudflare) contre Harvest Now, Decrypt Later."
|
|
278
|
+
})
|
|
279
|
+
else:
|
|
280
|
+
console.print(f"[bold green]🔒 Échange de clés hybride Post-Quantique actif sur {endpoint_label} ({negotiated_group})[/bold green]")
|
|
281
|
+
|
|
282
|
+
findings.append({
|
|
283
|
+
"file": endpoint_label,
|
|
284
|
+
"line": port,
|
|
285
|
+
"type": "Certificat (X.509)",
|
|
286
|
+
"detected": cert_sig_algo or f"Certificat leaf sur {cipher_name}",
|
|
287
|
+
"algo": "Certificat X.509 Asymétrique Classique (RSA/ECDSA)",
|
|
288
|
+
"risk": "MOYEN",
|
|
289
|
+
"remediation": "Inventorié dans le CBOM. Planifier la migration PKI vers des certificats hybrides ou ML-DSA (FIPS 204) d'ici 2028-2030."
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
return findings
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def generate_cyclonedx_cbom(findings: list, output_path: str):
|
|
296
|
+
"""Génère un fichier CBOM (Cryptography Bill of Materials) au format standard CycloneDX 1.6."""
|
|
297
|
+
cbom = {
|
|
298
|
+
"bomFormat": "CycloneDX",
|
|
299
|
+
"specVersion": "1.6",
|
|
300
|
+
"serialNumber": f"urn:uuid:{uuid.uuid4()}",
|
|
301
|
+
"version": 1,
|
|
302
|
+
"metadata": {
|
|
303
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
304
|
+
"tools": [{"vendor": "Shorless", "name": "shorless-scanner", "version": "0.8.6"}]
|
|
305
|
+
},
|
|
306
|
+
"components": []
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for idx, f in enumerate(findings):
|
|
310
|
+
cbom["components"].append({
|
|
311
|
+
"type": "cryptographic-asset",
|
|
312
|
+
"bom-ref": f"crypto-asset-{idx}",
|
|
313
|
+
"name": f["algo"],
|
|
314
|
+
"evidence": {
|
|
315
|
+
"occurrences": [{"location": f["file"], "line": f["line"]}]
|
|
316
|
+
},
|
|
317
|
+
"cryptoProperties": {
|
|
318
|
+
"assetType": "algorithm",
|
|
319
|
+
"quantumSafe": "false",
|
|
320
|
+
"remediation": f["remediation"]
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
Path(output_path).write_text(json.dumps(cbom, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@app.command()
|
|
328
|
+
def scan(
|
|
329
|
+
target_dir: str = typer.Argument(".", help="Dossier du projet à scanner"),
|
|
330
|
+
cbom_out: str = typer.Option("cbom.json", "--cbom", help="Fichier de sortie CycloneDX CBOM"),
|
|
331
|
+
check_tls: str = typer.Option(None, "--check-tls", help="URL ou domaine:port à inspecter en direct (ex: https://shorless.io)"),
|
|
332
|
+
fail_on_critical: bool = typer.Option(False, "--ci", help="Renvoie un code d'erreur (1) pour bloquer la CI/CD")
|
|
333
|
+
):
|
|
334
|
+
"""Lance un audit de vulnérabilité Post-Quantique sur le code, l'infra (Nginx/Docker) et les endpoints TLS."""
|
|
335
|
+
console.print(Panel.fit("[bold cyan]⚡ Shorless PQC-Scanner - Audit DevSecOps Post-Quantique[/bold cyan]"))
|
|
336
|
+
|
|
337
|
+
path = Path(target_dir)
|
|
338
|
+
all_findings = []
|
|
339
|
+
|
|
340
|
+
if path.exists():
|
|
341
|
+
# 1. Scan des fichiers Python (.py)
|
|
342
|
+
for py_file in path.rglob("*.py"):
|
|
343
|
+
if py_file.name in ("shorless_scanner.py", "pqc_scanner.py", "release_shorless.py", "cron_generate_article.py"):
|
|
344
|
+
continue
|
|
345
|
+
try:
|
|
346
|
+
tree = ast.parse(py_file.read_text(encoding="utf-8", errors="ignore"))
|
|
347
|
+
visitor = PythonCryptoVisitor(str(py_file))
|
|
348
|
+
visitor.visit(tree)
|
|
349
|
+
all_findings.extend(visitor.findings)
|
|
350
|
+
except SyntaxError:
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
# 2. Scan des configurations Nginx (.conf)
|
|
354
|
+
for conf_file in path.rglob("*.conf"):
|
|
355
|
+
all_findings.extend(scan_nginx_config(conf_file))
|
|
356
|
+
|
|
357
|
+
# 3. Scan des Dockerfiles (Dockerfile, Dockerfile.*, *.dockerfile)
|
|
358
|
+
for item in path.rglob("*"):
|
|
359
|
+
if item.is_file() and (
|
|
360
|
+
item.name == "Dockerfile"
|
|
361
|
+
or item.name.startswith("Dockerfile.")
|
|
362
|
+
or item.name.lower().endswith(".dockerfile")
|
|
363
|
+
):
|
|
364
|
+
all_findings.extend(scan_dockerfile(item))
|
|
365
|
+
|
|
366
|
+
# 4. Scan TLS en direct si --check-tls est fourni
|
|
367
|
+
if check_tls:
|
|
368
|
+
all_findings.extend(scan_live_tls_endpoint(check_tls))
|
|
369
|
+
|
|
370
|
+
# 5. Affichage des résultats dans le terminal
|
|
371
|
+
if not all_findings:
|
|
372
|
+
console.print("[bold green]✅ Aucune vulnérabilité post-quantique détectée ![/bold green]")
|
|
373
|
+
raise typer.Exit(0)
|
|
374
|
+
|
|
375
|
+
table = Table(title=f"Inventaire Cryptographique & Risques Quantiques ({len(all_findings)} alertes)")
|
|
376
|
+
table.add_column("Fichier / Endpoint", style="cyan")
|
|
377
|
+
table.add_column("Source", style="magenta")
|
|
378
|
+
table.add_column("Actif / Algorithme détecté", style="white")
|
|
379
|
+
table.add_column("Risque PQC", style="bold")
|
|
380
|
+
table.add_column("Plan de remédiation (Auto-PR disponible en SaaS)", style="green")
|
|
381
|
+
|
|
382
|
+
critical_count = 0
|
|
383
|
+
for f in all_findings:
|
|
384
|
+
risk_color = "red" if f["risk"] == "CRITIQUE" else "yellow"
|
|
385
|
+
if f["risk"] == "CRITIQUE":
|
|
386
|
+
critical_count += 1
|
|
387
|
+
table.add_row(
|
|
388
|
+
f"{f['file']}:{f['line']}",
|
|
389
|
+
f["type"],
|
|
390
|
+
f["algo"],
|
|
391
|
+
f"[{risk_color}]{f['risk']}[/{risk_color}]",
|
|
392
|
+
f["remediation"]
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
console.print(table)
|
|
396
|
+
|
|
397
|
+
# 6. Export du CBOM CycloneDX 1.6
|
|
398
|
+
generate_cyclonedx_cbom(all_findings, cbom_out)
|
|
399
|
+
console.print(f"\n📄 [bold]CBOM CycloneDX 1.6 généré avec succès :[/bold] [underline]{cbom_out}[/underline]")
|
|
400
|
+
|
|
401
|
+
# 7. Mode bloqueur CI/CD (GitHub Actions / GitLab CI)
|
|
402
|
+
if fail_on_critical and critical_count > 0:
|
|
403
|
+
console.print(f"\n[bold red]❌ Pipeline CI bloquée : {critical_count} vulnérabilité(s) critique(s) face au risque quantique.[/bold red]")
|
|
404
|
+
raise typer.Exit(code=1)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
if __name__ == "__main__":
|
|
408
|
+
app()
|