syfscan 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.
- ia/__init__.py +0 -0
- ia/claude_summarizer.py +121 -0
- output/__init__.py +0 -0
- output/html.py +96 -0
- output/json.py +57 -0
- output/pdf.py +74 -0
- output/terminal.py +72 -0
- parsers/requirement.py +41 -0
- scanner/__init__.py +0 -0
- scanner/osv.py +48 -0
- syfscan/__init__.py +0 -0
- syfscan/__main__.py +4 -0
- syfscan/cli.py +120 -0
- syfscan-0.1.0.dist-info/METADATA +4 -0
- syfscan-0.1.0.dist-info/RECORD +18 -0
- syfscan-0.1.0.dist-info/WHEEL +5 -0
- syfscan-0.1.0.dist-info/entry_points.txt +2 -0
- syfscan-0.1.0.dist-info/top_level.txt +5 -0
ia/__init__.py
ADDED
|
File without changes
|
ia/claude_summarizer.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# ia/claude_summarizer.py
|
|
2
|
+
import anthropic
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.rule import Rule
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
load_dotenv()
|
|
9
|
+
|
|
10
|
+
client = anthropic.Anthropic()
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def generate_summary(report):
|
|
16
|
+
vulnerabilities_text = build_vulnerabilities_text(report)
|
|
17
|
+
|
|
18
|
+
if not vulnerabilities_text:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
message = client.messages.create(
|
|
22
|
+
model="claude-opus-4-6",
|
|
23
|
+
max_tokens=400,
|
|
24
|
+
messages=[
|
|
25
|
+
{
|
|
26
|
+
"role": "user",
|
|
27
|
+
"content": f"""You are a security expert. Analyze these vulnerabilities and respond in exactly this format:
|
|
28
|
+
|
|
29
|
+
HOW TO FIX
|
|
30
|
+
[One paragraph. List every package to upgrade and to which version.]
|
|
31
|
+
|
|
32
|
+
RISKS
|
|
33
|
+
[One paragraph. What could concretely happen if left unpatched.]
|
|
34
|
+
|
|
35
|
+
ADDITIONAL MEASURES
|
|
36
|
+
[3 bullet points max. Practical extra steps the developer can take.]
|
|
37
|
+
|
|
38
|
+
Vulnerabilities:
|
|
39
|
+
{vulnerabilities_text}"""
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return message.content[0].text
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def display_summary(ai_text):
|
|
48
|
+
sections = {
|
|
49
|
+
"HOW TO FIX": ("π§ How to Fix", "green"),
|
|
50
|
+
"RISKS": ("β οΈ Risks", "red"),
|
|
51
|
+
"ADDITIONAL MEASURES": ("π‘οΈ Additional Measures", "yellow"),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.print()
|
|
55
|
+
console.rule("[bold]AI Summary[/bold]")
|
|
56
|
+
console.print()
|
|
57
|
+
|
|
58
|
+
current_section = None
|
|
59
|
+
current_lines = []
|
|
60
|
+
|
|
61
|
+
for line in ai_text.splitlines():
|
|
62
|
+
line = line.strip()
|
|
63
|
+
|
|
64
|
+
matched = False
|
|
65
|
+
for key in sections:
|
|
66
|
+
if key in line.upper():
|
|
67
|
+
if current_section and current_lines:
|
|
68
|
+
title, color = sections[current_section]
|
|
69
|
+
content = "\n".join(current_lines).strip()
|
|
70
|
+
console.print(f"[bold {color}]{title}[/bold {color}]")
|
|
71
|
+
console.print(content)
|
|
72
|
+
console.print()
|
|
73
|
+
|
|
74
|
+
current_section = key
|
|
75
|
+
current_lines = []
|
|
76
|
+
matched = True
|
|
77
|
+
break
|
|
78
|
+
|
|
79
|
+
if not matched and line:
|
|
80
|
+
current_lines.append(line)
|
|
81
|
+
|
|
82
|
+
# Flush last section
|
|
83
|
+
if current_section and current_lines:
|
|
84
|
+
title, color = sections[current_section]
|
|
85
|
+
content = "\n".join(current_lines).strip()
|
|
86
|
+
console.print(f"[bold {color}]{title}[/bold {color}]")
|
|
87
|
+
console.print(content)
|
|
88
|
+
console.print()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_fixed_version(vuln):
|
|
92
|
+
try:
|
|
93
|
+
for affected in vuln.get("affected", []):
|
|
94
|
+
for r in affected.get("ranges", []):
|
|
95
|
+
for event in r.get("events", []):
|
|
96
|
+
if "fixed" in event:
|
|
97
|
+
return event["fixed"]
|
|
98
|
+
except (KeyError, IndexError):
|
|
99
|
+
pass
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def build_vulnerabilities_text(report):
|
|
104
|
+
lines = []
|
|
105
|
+
|
|
106
|
+
for dep in report:
|
|
107
|
+
if not dep["vulnerabilites"]:
|
|
108
|
+
continue
|
|
109
|
+
for vuln in dep["vulnerabilites"]:
|
|
110
|
+
fixed = get_fixed_version(vuln)
|
|
111
|
+
severity = vuln.get("database_specific", {}).get("severity", "UNKNOWN")
|
|
112
|
+
line = (
|
|
113
|
+
f"- {dep['nom']} {dep['version']} : "
|
|
114
|
+
f"{vuln.get('id', 'UNKNOWN')} | "
|
|
115
|
+
f"{vuln.get('summary', 'No description')} | "
|
|
116
|
+
f"Severity: {severity} | "
|
|
117
|
+
f"Fixed in: {fixed or 'unknown'}"
|
|
118
|
+
)
|
|
119
|
+
lines.append(line)
|
|
120
|
+
|
|
121
|
+
return "\n".join(lines)
|
output/__init__.py
ADDED
|
File without changes
|
output/html.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from .terminal import get_label, get_score, score_couleur
|
|
3
|
+
|
|
4
|
+
# Aide IA pour cette fonction car orange3 n'existe pas en HTML, et orange n'offre pas le bon affichage au niveau du terminal
|
|
5
|
+
def convert_couleur_html(couleur):
|
|
6
|
+
map = {
|
|
7
|
+
"red":"red",
|
|
8
|
+
"yellow":"gold",
|
|
9
|
+
"orange3":"orange"
|
|
10
|
+
}
|
|
11
|
+
return map.get(couleur, "black")
|
|
12
|
+
|
|
13
|
+
def rapport_html (rapport, output_path, max_vulns):
|
|
14
|
+
|
|
15
|
+
#https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_span
|
|
16
|
+
#https://www-sololearn-com.translate.goog/en/Discuss/2715062/how-to-code-html-in-python?_x_tr_sl=en&_x_tr_tl=fr&_x_tr_hl=fr&_x_tr_pto=rq#
|
|
17
|
+
|
|
18
|
+
html="""
|
|
19
|
+
<!DOCTYPE html>
|
|
20
|
+
<html>
|
|
21
|
+
<head>
|
|
22
|
+
<meta charset ="UTF-8">
|
|
23
|
+
<style>
|
|
24
|
+
|
|
25
|
+
body {
|
|
26
|
+
background-color: white;
|
|
27
|
+
font-family: arial;
|
|
28
|
+
}
|
|
29
|
+
h1{
|
|
30
|
+
color: black
|
|
31
|
+
}
|
|
32
|
+
h2{
|
|
33
|
+
color: darkred
|
|
34
|
+
}
|
|
35
|
+
p{
|
|
36
|
+
color: black
|
|
37
|
+
}
|
|
38
|
+
</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<h1>Rapport syfscan </h1>
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
for dep in rapport:
|
|
47
|
+
nom = dep["nom"]
|
|
48
|
+
version = dep["version"]
|
|
49
|
+
vulns = dep["vulnerabilites"]
|
|
50
|
+
|
|
51
|
+
if max_vulns is not None:
|
|
52
|
+
vulns = vulns[:max_vulns]
|
|
53
|
+
couleur_h2 = "green" if not vulns else"darkred"
|
|
54
|
+
|
|
55
|
+
html+=f"<h2 style='color:{couleur_h2}'>{nom} {version}</h2>"
|
|
56
|
+
|
|
57
|
+
if not vulns:
|
|
58
|
+
html+=f"<p>Aucune vulnΓ©rabilitΓ© dΓ©tectΓ©e</p>"
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
html+=f"<p>{len(vulns)} vulnΓ©rabilitΓ©s dΓ©tectΓ©es"
|
|
63
|
+
for v in vulns:
|
|
64
|
+
|
|
65
|
+
score = get_score(v)
|
|
66
|
+
couleur = convert_couleur_html(score_couleur(score))
|
|
67
|
+
cve = get_label(v)
|
|
68
|
+
summary = v.get("summary", "no description")
|
|
69
|
+
|
|
70
|
+
html+=f"""
|
|
71
|
+
<p>
|
|
72
|
+
<span style="color:{couleur}; font-weight:bold">{cve}
|
|
73
|
+
</span>
|
|
74
|
+
> Score: {score} <br>
|
|
75
|
+
{summary}
|
|
76
|
+
</p>
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
html+="""
|
|
82
|
+
</body>
|
|
83
|
+
</html>
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
with open(output_path, "w", encoding="utf-8") as f :
|
|
88
|
+
f.write(html)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
output/json.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from .terminal import get_label, get_score, score_couleur
|
|
3
|
+
|
|
4
|
+
def rapport_json (rapport, output_path, max_vulns):
|
|
5
|
+
|
|
6
|
+
results =[]
|
|
7
|
+
|
|
8
|
+
for dep in rapport:
|
|
9
|
+
nom = dep["nom"]
|
|
10
|
+
version = dep["version"]
|
|
11
|
+
vulns = dep["vulnerabilites"]
|
|
12
|
+
|
|
13
|
+
if max_vulns is not None:
|
|
14
|
+
vulns = vulns[:max_vulns]
|
|
15
|
+
|
|
16
|
+
if not vulns:
|
|
17
|
+
results.append({
|
|
18
|
+
"nom": nom,
|
|
19
|
+
"version": version,
|
|
20
|
+
"statut" : "non vulnerable",
|
|
21
|
+
"nombre de vulnerabilite(s)": 0,
|
|
22
|
+
|
|
23
|
+
},)
|
|
24
|
+
continue
|
|
25
|
+
|
|
26
|
+
vulns_list=[]
|
|
27
|
+
|
|
28
|
+
for v in vulns:
|
|
29
|
+
|
|
30
|
+
score = get_score(v)
|
|
31
|
+
couleur = score_couleur(score)
|
|
32
|
+
cve = get_label(v)
|
|
33
|
+
summary = v.get("summary", "no description")
|
|
34
|
+
vulns_list.append({
|
|
35
|
+
"score": score,
|
|
36
|
+
"cve": cve,
|
|
37
|
+
"summary": summary,
|
|
38
|
+
},)
|
|
39
|
+
|
|
40
|
+
results.append({
|
|
41
|
+
"nom": nom,
|
|
42
|
+
"version": version,
|
|
43
|
+
"statut" : "vulnerable",
|
|
44
|
+
"nombre de vulnerabilite(s)": len(vulns),
|
|
45
|
+
"vulnerabilites": vulns_list
|
|
46
|
+
},)
|
|
47
|
+
|
|
48
|
+
with open(output_path, "w") as f :
|
|
49
|
+
json.dump(results, f, indent=4) #conversion en json de la liste results
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
output/pdf.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from reportlab.lib.pagesizes import letter
|
|
2
|
+
from reportlab.pdfgen import canvas
|
|
3
|
+
from reportlab.lib import colors
|
|
4
|
+
from .terminal import get_label, get_score, score_couleur
|
|
5
|
+
def rapport_pdf(rapport, output_path, max_vulns=None):
|
|
6
|
+
c = canvas.Canvas(str(output_path), pagesize=letter)
|
|
7
|
+
width, height = letter
|
|
8
|
+
|
|
9
|
+
y = height - 40
|
|
10
|
+
|
|
11
|
+
c.setFont("Helvetica-Bold", 14)
|
|
12
|
+
c.drawString(40, y, "Rapport SyfScan - VulnΓ©rabilitΓ©s")
|
|
13
|
+
|
|
14
|
+
y -= 30
|
|
15
|
+
|
|
16
|
+
c.setFont("Helvetica", 10)
|
|
17
|
+
|
|
18
|
+
for dep in rapport:
|
|
19
|
+
nom = dep.get("nom")
|
|
20
|
+
version = dep.get("version")
|
|
21
|
+
vulns = dep.get("vulnerabilites", [])
|
|
22
|
+
|
|
23
|
+
if max_vulns:
|
|
24
|
+
vulns = vulns[:max_vulns]
|
|
25
|
+
if not vulns :
|
|
26
|
+
c.setFillColor(colors.green)
|
|
27
|
+
else:
|
|
28
|
+
c.setFillColor(colors.red)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
c.setFont("Helvetica-Bold", 11)
|
|
32
|
+
c.drawString(40, y, f"{nom} ({version})")
|
|
33
|
+
y -= 15
|
|
34
|
+
|
|
35
|
+
if not vulns:
|
|
36
|
+
|
|
37
|
+
c.setFont("Helvetica", 10)
|
|
38
|
+
c.drawString(60, y, "Non vulnΓ©rable")
|
|
39
|
+
y -= 20
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
c.setFont("Helvetica", 10)
|
|
46
|
+
c.drawString(60, y, f"{len(vulns)} vulnΓ©rabilitΓ©(s)")
|
|
47
|
+
y -= 15
|
|
48
|
+
|
|
49
|
+
for v in vulns:
|
|
50
|
+
#cve = v.get("id", "N/A")
|
|
51
|
+
cve=get_label(v)
|
|
52
|
+
summary = v.get("summary", "no description")
|
|
53
|
+
score = get_score(v)
|
|
54
|
+
couleur = score_couleur(score)
|
|
55
|
+
text = f"- {cve}: {summary[:60]}"
|
|
56
|
+
|
|
57
|
+
if couleur =="red":
|
|
58
|
+
c.setFillColor(colors.red)
|
|
59
|
+
elif couleur =="orange3":
|
|
60
|
+
c.setFillColor(colors.orange)
|
|
61
|
+
else:
|
|
62
|
+
c.setFillColor(colors.gold)
|
|
63
|
+
|
|
64
|
+
if y < 40:
|
|
65
|
+
c.showPage()
|
|
66
|
+
c.setFont("Helvetica", 10)
|
|
67
|
+
y = height - 40
|
|
68
|
+
|
|
69
|
+
c.drawString(80, y, text)
|
|
70
|
+
y -= 15
|
|
71
|
+
|
|
72
|
+
y -= 10
|
|
73
|
+
|
|
74
|
+
c.save()
|
output/terminal.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
|
|
3
|
+
console = Console()
|
|
4
|
+
|
|
5
|
+
# https://ossf.github.io/osv-schema/#database_specific-field
|
|
6
|
+
def get_score(vuln):
|
|
7
|
+
# Si on a le score dans severity[]
|
|
8
|
+
for severity in vuln.get("severity", []):
|
|
9
|
+
if severity.get("type") in ("CVSS_V3", "CVSS_V2"):
|
|
10
|
+
try:
|
|
11
|
+
return float(severity.get("score", 0.0))
|
|
12
|
+
except ValueError:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
score_dictionnary = {"CRITICAL": 9.5, "HIGH": 8.0, "MEDIUM": 5.0, "LOW": 2.0}
|
|
16
|
+
|
|
17
|
+
# Si le score est dans database_specific
|
|
18
|
+
db_severity = vuln.get("database_specific", {}).get("severity", "")
|
|
19
|
+
if db_severity.upper() in score_dictionnary:
|
|
20
|
+
return score_dictionnary[db_severity.upper()]
|
|
21
|
+
|
|
22
|
+
# Si le score est dans affected[0].ecosystem_specific
|
|
23
|
+
affected = vuln.get("affected", [])
|
|
24
|
+
if affected:
|
|
25
|
+
eco_severity = affected[0].get("ecosystem_specific", {}).get("severity", "")
|
|
26
|
+
if eco_severity.upper() in score_dictionnary:
|
|
27
|
+
return score_dictionnary[eco_sev.upper()]
|
|
28
|
+
|
|
29
|
+
return 0.0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def score_couleur(score):
|
|
33
|
+
if score >= 9.0:
|
|
34
|
+
return "red"
|
|
35
|
+
elif score >= 7.0:
|
|
36
|
+
return "orange3"
|
|
37
|
+
else:
|
|
38
|
+
return "yellow"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Fonction faite avec IA
|
|
42
|
+
def get_label(vuln):
|
|
43
|
+
aliases = vuln.get("aliases", [])
|
|
44
|
+
return next((a for a in aliases if a.startswith("CVE-")), vuln.get("id", "N/A"))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def display_report(rapport, max_vulns=None):
|
|
48
|
+
console.print("\n[bold green]// Rapport syfscan[/bold green]\n")
|
|
49
|
+
|
|
50
|
+
for dep in rapport:
|
|
51
|
+
nom = dep["nom"]
|
|
52
|
+
version = dep["version"]
|
|
53
|
+
vulns = dep["vulnerabilites"]
|
|
54
|
+
|
|
55
|
+
if max_vulns is not None:
|
|
56
|
+
vulns = vulns[:max_vulns]
|
|
57
|
+
|
|
58
|
+
if not vulns:
|
|
59
|
+
console.print(f"[green]{nom} {version} β 0 vulnΓ©rabilitΓ©[/green]")
|
|
60
|
+
console.print()
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
console.print(f"[red]{nom} {version} β {len(vulns)} vulnΓ©rabilitΓ©(s)[/red]")
|
|
64
|
+
|
|
65
|
+
for v in vulns:
|
|
66
|
+
score = get_score(v)
|
|
67
|
+
couleur = score_couleur(score)
|
|
68
|
+
cve = get_label(v)
|
|
69
|
+
summary = v.get("summary", "no description")
|
|
70
|
+
console.print(f" [dim]>[/dim] [{couleur}]{cve} β {summary}[/{couleur}]")
|
|
71
|
+
|
|
72
|
+
console.print()
|
parsers/requirement.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from packaging.requirements import Requirement
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
|
|
4
|
+
console = Console()
|
|
5
|
+
|
|
6
|
+
def parse_requirements(fichier):
|
|
7
|
+
finale_liste =[]
|
|
8
|
+
try :
|
|
9
|
+
with open (fichier, "r") as f :
|
|
10
|
+
lignes = f.readlines()
|
|
11
|
+
|
|
12
|
+
for ligne in lignes :
|
|
13
|
+
ligne = ligne.strip()
|
|
14
|
+
|
|
15
|
+
if not ligne or ligne.startswith('#'):
|
|
16
|
+
continue
|
|
17
|
+
|
|
18
|
+
try :
|
|
19
|
+
req = Requirement(ligne)
|
|
20
|
+
nom = req.name
|
|
21
|
+
version ="0.0.0"
|
|
22
|
+
|
|
23
|
+
if req.specifier :#determiner la bonne version si elle existe (borne basse)
|
|
24
|
+
for spec in req.specifier :
|
|
25
|
+
if spec.operator in ["==", ">=", "~=", "==="]:
|
|
26
|
+
version =spec.version
|
|
27
|
+
break
|
|
28
|
+
finale_liste.append({"nom":nom, "version" :version})
|
|
29
|
+
|
|
30
|
+
except Exception :
|
|
31
|
+
console.print(f"[yellow][ WARNING ][/yellow] [dim]Ligne ignorΓ©e β[/dim] [cyan]{ligne}[/cyan]")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
except Exception as e :
|
|
37
|
+
console.print(f"[bold red][ ERROR ][/bold red] [red]impossible de lire le fichier β {e}[/red]")
|
|
38
|
+
cons
|
|
39
|
+
|
|
40
|
+
console.print()
|
|
41
|
+
return finale_liste
|
scanner/__init__.py
ADDED
|
File without changes
|
scanner/osv.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
|
|
4
|
+
from output.terminal import get_score
|
|
5
|
+
|
|
6
|
+
console = Console()
|
|
7
|
+
|
|
8
|
+
OSV_API_URL = "https://api.osv.dev/v1/query"
|
|
9
|
+
|
|
10
|
+
def scan_dependencies(dependencies):
|
|
11
|
+
|
|
12
|
+
rapport = []
|
|
13
|
+
|
|
14
|
+
for dep in dependencies:
|
|
15
|
+
nom = dep["nom"]
|
|
16
|
+
version = dep["version"]
|
|
17
|
+
|
|
18
|
+
console.print(f"[green]>[/green] [dim]Scan[/dim] [cyan]{nom}[/cyan] [dim]{version}[/dim]")
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
response = requests.post(OSV_API_URL, json={
|
|
22
|
+
"package": {
|
|
23
|
+
"name": nom,
|
|
24
|
+
"ecosystem": "PyPI"
|
|
25
|
+
},
|
|
26
|
+
"version": version
|
|
27
|
+
})
|
|
28
|
+
response.raise_for_status()
|
|
29
|
+
data = response.json()
|
|
30
|
+
|
|
31
|
+
vulns = sorted(data.get("vulns", []), key=get_score, reverse=True)
|
|
32
|
+
|
|
33
|
+
rapport.append({
|
|
34
|
+
"nom": nom,
|
|
35
|
+
"version": version,
|
|
36
|
+
"vulnerabilites": vulns
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
except requests.exceptions.RequestException as e:
|
|
40
|
+
console.print(f"[bold red][ ERROR ][/bold red] [red]{nom} β {e}[/red]")
|
|
41
|
+
rapport.append({
|
|
42
|
+
"nom": nom,
|
|
43
|
+
"version": version,
|
|
44
|
+
"vulnerabilites": [],
|
|
45
|
+
"erreur": str(e)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return rapport
|
syfscan/__init__.py
ADDED
|
File without changes
|
syfscan/__main__.py
ADDED
syfscan/cli.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
from parsers.requirement import parse_requirements
|
|
7
|
+
from scanner.osv import scan_dependencies
|
|
8
|
+
from ia.claude_summarizer import generate_summary, display_summary
|
|
9
|
+
from output.terminal import display_report
|
|
10
|
+
from output.json import rapport_json
|
|
11
|
+
from output.html import rapport_html
|
|
12
|
+
from output.pdf import rapport_pdf
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
BANNER = """
|
|
17
|
+
[green]
|
|
18
|
+
βββββββββββ βββββββββββββββββββ βββββββ ββββββ ββββ βββ
|
|
19
|
+
ββββββββββββ βββββββββββββββββββββββββββββββββββββββββ βββ
|
|
20
|
+
ββββββββ βββββββ ββββββ βββββββββββ ββββββββββββββ βββ
|
|
21
|
+
ββββββββ βββββ ββββββ βββββββββββ ββββββββββββββββββ
|
|
22
|
+
ββββββββ βββ βββ βββββββββββββββββββ ββββββ ββββββ
|
|
23
|
+
ββββββββ βββ βββ ββββββββ ββββββββββ ββββββ βββββ
|
|
24
|
+
[/green][dim green] v1.0 β Python dependency vulnerability scanner. [/dim green]
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def main():
|
|
28
|
+
# Fait avec https://docs.python.org/3/library/argparse.html
|
|
29
|
+
parser = argparse.ArgumentParser(
|
|
30
|
+
prog="syfscan",
|
|
31
|
+
description="SyfScan β DΓ©tecteur de vulnΓ©rabilitΓ©s de dΓ©pendances",
|
|
32
|
+
epilog="Exemple : syfscan requirements.txt"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"file",
|
|
37
|
+
help="Chemin vers le fichier requirements.txt"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"-xvuln",
|
|
42
|
+
type=int,
|
|
43
|
+
default=None,
|
|
44
|
+
metavar="X",
|
|
45
|
+
help="Afficher uniquement les X vulnΓ©rabilitΓ©s les plus graves (ex: -xvuln 5)"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--no-ai",
|
|
50
|
+
action="store_true",
|
|
51
|
+
help="DΓ©sactiver le rΓ©sumΓ© IA"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--json",
|
|
56
|
+
action="store_true",
|
|
57
|
+
help="GΓ©nΓ©rer un fichier au format JSON"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"--html",
|
|
62
|
+
action="store_true",
|
|
63
|
+
help="GΓ©nΓ©rer un fichier au format HTML"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--pdf",
|
|
68
|
+
action="store_true",
|
|
69
|
+
help="GΓ©nΓ©rer un fichier au format PDF"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
args = parser.parse_args()
|
|
74
|
+
file = Path(args.file)
|
|
75
|
+
|
|
76
|
+
console.print(BANNER)
|
|
77
|
+
|
|
78
|
+
# Verification validitΓ© des entrΓ©es clients
|
|
79
|
+
if not file.exists():
|
|
80
|
+
console.print(f"[bold red][ERREUR][/bold red] Fichier '{file}' introuvable.")
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if file.name != "requirements.txt":
|
|
86
|
+
console.print("[bold red][ERREUR][/bold red] SyfScan accepte uniquement les fichiers requirements.txt")
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
|
|
89
|
+
# Parsing
|
|
90
|
+
console.print(f"[green]>[/green] [dim]Fichier cible :[/dim][cyan]{file}[/cyan]")
|
|
91
|
+
dependencies = parse_requirements(file) # fonction parse Γ rΓ©cupΓ©rer dans parser/requirements.py
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Interrogation OSV.dev
|
|
95
|
+
report = scan_dependencies(dependencies) # fonction scan_dependencies Γ rΓ©cupΓ©rer dans scanner/osv.py
|
|
96
|
+
|
|
97
|
+
# Output
|
|
98
|
+
|
|
99
|
+
#permet de choisir sous quel format exporter les donnΓ©es, par defaut dans le terminal
|
|
100
|
+
if args.json:
|
|
101
|
+
rapport_json(report, Path("rapport.json"), args.xvuln)
|
|
102
|
+
console.print("\n[bold green]Rapport JSON gΓ©nΓ©rΓ© :[/bold green] rapport.json\n")
|
|
103
|
+
elif args.html:
|
|
104
|
+
rapport_html(report, Path("rapport.html"), args.xvuln)
|
|
105
|
+
console.print("\n[bold green]Rapport HTML gΓ©nΓ©rΓ© :[/bold green] rapport.html\n")
|
|
106
|
+
elif args.pdf:
|
|
107
|
+
rapport_pdf(report, Path("rapport.pdf"), args.xvuln)
|
|
108
|
+
console.print("\n[bold green]Rapport PDF gΓ©nΓ©rΓ© :[/bold green] rapport.pdf\n")
|
|
109
|
+
|
|
110
|
+
else :
|
|
111
|
+
display_report(report, args.xvuln)
|
|
112
|
+
|
|
113
|
+
# RΓ©sumΓ© IA
|
|
114
|
+
if not args.no_ai:
|
|
115
|
+
print("Generating AI summary...")
|
|
116
|
+
ai_report = generate_summary(report)
|
|
117
|
+
display_summary(ai_report)
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__" :
|
|
120
|
+
main()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
ia/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
ia/claude_summarizer.py,sha256=bX2XOZZFZchMhFc1JX6Z5aeLjEyG9Eman7HHoKlFrQk,3385
|
|
3
|
+
output/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
output/html.py,sha256=2WiqM__dQhE5u_WhwqPfY3yhzek00SHYB1D8OjTIgQg,2393
|
|
5
|
+
output/json.py,sha256=Yvf2lfhmArpPW3HIvkXrGSqAqCB6aSE1Uajtwvx_teI,1322
|
|
6
|
+
output/pdf.py,sha256=jNW2fLfwZpxGLefqfdjJz9Q8HO7556He2Te483cqgL0,1917
|
|
7
|
+
output/terminal.py,sha256=uK1vj1VNXgykBaVsuOeqKWf-mnZ8kENyvXti7Ag7ucQ,2216
|
|
8
|
+
parsers/requirement.py,sha256=pbJh-G-6StlNT3nPTWlh3Pr5Fe6CexFaS30uL70ykbQ,1390
|
|
9
|
+
scanner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
scanner/osv.py,sha256=ZyRbTxj_qgN7m2yf1fDPOe_nQbjLjOE4_8cK1QHPoyc,1302
|
|
11
|
+
syfscan/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
syfscan/__main__.py,sha256=WDqExrefwrXk1MOBmjwtXPnIihVito76Cd1vcfnFxTI,66
|
|
13
|
+
syfscan/cli.py,sha256=rNKvzelSr0ZIuK9askkfohhqBMtrVBHogMu7aheBEag,4266
|
|
14
|
+
syfscan-0.1.0.dist-info/METADATA,sha256=Ajv03QPBOORZq5gv_7wWc2uvprOSc7R9RQk8ur__zIM,107
|
|
15
|
+
syfscan-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
16
|
+
syfscan-0.1.0.dist-info/entry_points.txt,sha256=Xil9FSfre8F43iD4aBexk4GSMsKurKm5YhnSO_YC3wc,50
|
|
17
|
+
syfscan-0.1.0.dist-info/top_level.txt,sha256=PnPb582uRcmoEcQe3TcydY9IteXc-ZhlX0JzPJRNj6M,34
|
|
18
|
+
syfscan-0.1.0.dist-info/RECORD,,
|