syfscan 0.1.0__tar.gz
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.
- syfscan-0.1.0/PKG-INFO +4 -0
- syfscan-0.1.0/README.md +155 -0
- syfscan-0.1.0/ia/__init__.py +0 -0
- syfscan-0.1.0/ia/claude_summarizer.py +121 -0
- syfscan-0.1.0/output/__init__.py +0 -0
- syfscan-0.1.0/output/html.py +96 -0
- syfscan-0.1.0/output/json.py +57 -0
- syfscan-0.1.0/output/pdf.py +74 -0
- syfscan-0.1.0/output/terminal.py +72 -0
- syfscan-0.1.0/parsers/requirement.py +41 -0
- syfscan-0.1.0/pyproject.toml +16 -0
- syfscan-0.1.0/scanner/__init__.py +0 -0
- syfscan-0.1.0/scanner/osv.py +48 -0
- syfscan-0.1.0/setup.cfg +4 -0
- syfscan-0.1.0/syfscan/__init__.py +0 -0
- syfscan-0.1.0/syfscan/__main__.py +4 -0
- syfscan-0.1.0/syfscan/cli.py +120 -0
- syfscan-0.1.0/syfscan.egg-info/PKG-INFO +4 -0
- syfscan-0.1.0/syfscan.egg-info/SOURCES.txt +20 -0
- syfscan-0.1.0/syfscan.egg-info/dependency_links.txt +1 -0
- syfscan-0.1.0/syfscan.egg-info/entry_points.txt +2 -0
- syfscan-0.1.0/syfscan.egg-info/top_level.txt +7 -0
syfscan-0.1.0/PKG-INFO
ADDED
syfscan-0.1.0/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# SyfScan
|
|
2
|
+
|
|
3
|
+
> Dependency vulnerability scanner for Python projects — powered by [OSV.dev](https://osv.dev) and Claude AI.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
SyfScan is a command-line tool that scans your `requirements.txt` against the [OSV.dev](https://osv.dev) vulnerability database. It displays a severity-ranked report directly in your terminal and optionally generates a plain-language security summary using Claude AI — making security accessible even to developers unfamiliar with CVEs.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- 🔍 Scans all dependencies in `requirements.txt` against OSV.dev
|
|
16
|
+
- 🎨 Color-coded terminal output ranked by severity
|
|
17
|
+
- 🤖 AI-generated summary with risks, fixes, and recommendations via Claude
|
|
18
|
+
- 📄 Export report to JSON, HTML, or PDF
|
|
19
|
+
- ⚡ Fast and lightweight — no local database required
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
- Python 3.12+
|
|
26
|
+
- An Anthropic API key *(optional — only required for AI summary)*
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
git clone https://github.com/yourname/syfscan
|
|
34
|
+
cd syfscan
|
|
35
|
+
pip install -e .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
If you want the AI summary, create a `.env` file at the root of the project:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# .env
|
|
42
|
+
ANTHROPIC_API_KEY=sk-ant-...
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
> ⚠️ Never commit your `.env` file. Add it to `.gitignore`.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Full scan with AI summary
|
|
53
|
+
syfscan requirements.txt
|
|
54
|
+
|
|
55
|
+
# Scan without AI summary
|
|
56
|
+
syfscan requirements.txt --no-ai
|
|
57
|
+
|
|
58
|
+
# Show only the N most critical vulnerabilities per package
|
|
59
|
+
syfscan requirements.txt -xvuln 5
|
|
60
|
+
|
|
61
|
+
# Export report to JSON
|
|
62
|
+
syfscan requirements.txt --json
|
|
63
|
+
|
|
64
|
+
# Export report to HTML
|
|
65
|
+
syfscan requirements.txt --html
|
|
66
|
+
|
|
67
|
+
# Export report to PDF
|
|
68
|
+
syfscan requirements.txt --pdf
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Options
|
|
74
|
+
|
|
75
|
+
| Flag | Description |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `--no-ai` | Disable Claude AI summary |
|
|
78
|
+
| `-xvuln N` | Limit output to the N most critical vulnerabilities per package |
|
|
79
|
+
| `--json` | Export report to `rapport.json` |
|
|
80
|
+
| `--html` | Export report to `rapport.html` |
|
|
81
|
+
| `--pdf` | Export report to `rapport.pdf` |
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Example output
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
███████╗██╗ ██╗███████╗███████╗ ██████╗ █████╗ ███╗ ██╗
|
|
89
|
+
██╔════╝╚██╗ ██╔╝██╔════╝██╔════╝██╔════╝██╔══██╗████╗ ██║
|
|
90
|
+
███████╗ ╚████╔╝ █████╗ ███████╗██║ ███████║██╔██╗ ██║
|
|
91
|
+
╚════██║ ╚██╔╝ ██╔══╝ ╚════██║██║ ██╔══██║██║╚██╗██║
|
|
92
|
+
███████║ ██║ ██║ ███████║╚██████╗██║ ██║██║ ╚████║
|
|
93
|
+
╚══════╝ ╚═╝ ╚═╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═══╝
|
|
94
|
+
v1.0 — Python dependency vulnerability scanner.
|
|
95
|
+
|
|
96
|
+
> Fichier cible : requirements.txt
|
|
97
|
+
|
|
98
|
+
> Scan numpy 2.2.2
|
|
99
|
+
> Scan pillow 12.1.1
|
|
100
|
+
...
|
|
101
|
+
|
|
102
|
+
pillow 12.1.1 — 6 vulnérabilité(s)
|
|
103
|
+
> CVE-2026-42311 — OOB Write with Invalid PSD Tile Extents
|
|
104
|
+
> CVE-2026-40192 — FITS GZIP decompression bomb
|
|
105
|
+
> CVE-2026-42309 — Heap buffer overflow with nested list coordinates
|
|
106
|
+
> CVE-2026-42310 — PDF Parsing Trailer Infinite Loop (DoS)
|
|
107
|
+
> CVE-2026-42308 — Integer overflow when processing fonts
|
|
108
|
+
|
|
109
|
+
────────────────── AI Summary ──────────────────
|
|
110
|
+
|
|
111
|
+
🔧 How to Fix
|
|
112
|
+
Upgrade Pillow from 12.1.1 to 12.2.0. Run pip install --upgrade Pillow.
|
|
113
|
+
|
|
114
|
+
⚠️ Risks
|
|
115
|
+
Attackers could achieve arbitrary code execution, denial of service,
|
|
116
|
+
or full system compromise via malicious image or document files.
|
|
117
|
+
|
|
118
|
+
🛡️ Additional Measures
|
|
119
|
+
- Validate and sanitize all uploaded files before processing.
|
|
120
|
+
- Run image workloads in a sandboxed environment.
|
|
121
|
+
- Enable automated dependency scanning in your CI/CD pipeline.
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Project structure
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
syfscan/
|
|
130
|
+
├── syfscan/
|
|
131
|
+
│ ├── cli.py
|
|
132
|
+
│ └── __main__.py
|
|
133
|
+
├── parsers/
|
|
134
|
+
│ └── requirement.py
|
|
135
|
+
├── scanner/
|
|
136
|
+
│ └── osv.py
|
|
137
|
+
├── output/
|
|
138
|
+
│ ├── terminal.py
|
|
139
|
+
│ ├── json.py
|
|
140
|
+
│ ├── html.py
|
|
141
|
+
│ └── pdf.py
|
|
142
|
+
├── ia/
|
|
143
|
+
│ └── claude_summarizer.py
|
|
144
|
+
├── test/
|
|
145
|
+
├── .env
|
|
146
|
+
├── .gitignore
|
|
147
|
+
├── pyproject.toml
|
|
148
|
+
└── README.md
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT — see [LICENSE](LICENSE) for details.
|
|
File without changes
|
|
@@ -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)
|
|
File without changes
|
|
@@ -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
|
+
|
|
@@ -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
|
+
|
|
@@ -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()
|
|
@@ -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()
|
|
@@ -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
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# https://www.reddit.com/r/learnpython/comments/182cdyz/how_can_i_create_a_command_line_tool_and_execute/
|
|
2
|
+
# https://ycopin.pages.in2p3.fr/Informatique-Python/Cours/packaging.html
|
|
3
|
+
|
|
4
|
+
[project]
|
|
5
|
+
name = "syfscan"
|
|
6
|
+
description = "Détecteur de vulnérabilités de dépendances"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
|
|
9
|
+
[project.scripts]
|
|
10
|
+
syfscan = "syfscan.__main__:main"
|
|
11
|
+
|
|
12
|
+
[tool.setuptools.packages]
|
|
13
|
+
find = {}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
File without changes
|
|
@@ -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-0.1.0/setup.cfg
ADDED
|
File without changes
|
|
@@ -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,20 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
ia/__init__.py
|
|
4
|
+
ia/claude_summarizer.py
|
|
5
|
+
output/__init__.py
|
|
6
|
+
output/html.py
|
|
7
|
+
output/json.py
|
|
8
|
+
output/pdf.py
|
|
9
|
+
output/terminal.py
|
|
10
|
+
parsers/requirement.py
|
|
11
|
+
scanner/__init__.py
|
|
12
|
+
scanner/osv.py
|
|
13
|
+
syfscan/__init__.py
|
|
14
|
+
syfscan/__main__.py
|
|
15
|
+
syfscan/cli.py
|
|
16
|
+
syfscan.egg-info/PKG-INFO
|
|
17
|
+
syfscan.egg-info/SOURCES.txt
|
|
18
|
+
syfscan.egg-info/dependency_links.txt
|
|
19
|
+
syfscan.egg-info/entry_points.txt
|
|
20
|
+
syfscan.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|