forceupdate 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.
Files changed (50) hide show
  1. forceupdate/__init__.py +9 -0
  2. forceupdate/__main__.py +5 -0
  3. forceupdate/advisories/__init__.py +30 -0
  4. forceupdate/advisories/cvss.py +95 -0
  5. forceupdate/advisories/osv.py +584 -0
  6. forceupdate/checks/__init__.py +49 -0
  7. forceupdate/checks/base.py +98 -0
  8. forceupdate/checks/deprecated.py +115 -0
  9. forceupdate/checks/outdated.py +183 -0
  10. forceupdate/checks/priority.py +82 -0
  11. forceupdate/checks/vulns.py +201 -0
  12. forceupdate/cli.py +309 -0
  13. forceupdate/detect/__init__.py +5 -0
  14. forceupdate/detect/languages.py +266 -0
  15. forceupdate/detect/manifests.py +478 -0
  16. forceupdate/engine.py +313 -0
  17. forceupdate/models.py +510 -0
  18. forceupdate/net/__init__.py +20 -0
  19. forceupdate/net/cache.py +217 -0
  20. forceupdate/net/http.py +270 -0
  21. forceupdate/parsers/__init__.py +43 -0
  22. forceupdate/parsers/base.py +22 -0
  23. forceupdate/parsers/json_spans.py +123 -0
  24. forceupdate/parsers/lockfiles.py +231 -0
  25. forceupdate/parsers/npm.py +1257 -0
  26. forceupdate/parsers/python.py +1421 -0
  27. forceupdate/parsers/toml_spans.py +297 -0
  28. forceupdate/registries/__init__.py +52 -0
  29. forceupdate/registries/base.py +294 -0
  30. forceupdate/registries/npm.py +153 -0
  31. forceupdate/registries/pypi.py +242 -0
  32. forceupdate/report/__init__.py +359 -0
  33. forceupdate/report/json.py +34 -0
  34. forceupdate/report/markdown.py +169 -0
  35. forceupdate/report/terminal.py +376 -0
  36. forceupdate/sources/__init__.py +1 -0
  37. forceupdate/sources/base.py +45 -0
  38. forceupdate/sources/local.py +183 -0
  39. forceupdate/update/__init__.py +8 -0
  40. forceupdate/update/constraints.py +317 -0
  41. forceupdate/update/planner.py +531 -0
  42. forceupdate/versions/__init__.py +40 -0
  43. forceupdate/versions/base.py +229 -0
  44. forceupdate/versions/npm.py +432 -0
  45. forceupdate/versions/pypi.py +220 -0
  46. forceupdate-0.1.0.dist-info/METADATA +79 -0
  47. forceupdate-0.1.0.dist-info/RECORD +50 -0
  48. forceupdate-0.1.0.dist-info/WHEEL +4 -0
  49. forceupdate-0.1.0.dist-info/entry_points.txt +2 -0
  50. forceupdate-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,9 @@
1
+ """forceupdate : trouve les dépendances à mettre à jour, et fait les mises à jour.
2
+
3
+ La version vient des métadonnées du paquet installé : elle n'est écrite qu'à un
4
+ seul endroit, dans ``pyproject.toml``.
5
+ """
6
+
7
+ from importlib.metadata import version
8
+
9
+ __version__ = version("forceupdate")
@@ -0,0 +1,5 @@
1
+ """``python -m forceupdate`` : la même chose que la commande ``forceupdate``."""
2
+
3
+ from forceupdate.cli import app
4
+
5
+ app(prog_name="forceupdate")
@@ -0,0 +1,30 @@
1
+ """Les avis de sécurité : les failles connues d'une version de paquet (type 2 du brief).
2
+
3
+ - ``osv.py`` : le client d'OSV (``Osv``), et ``Advisory``, un avis réduit à ce qui sert,
4
+ avec la version qui corrige la faille (``Advisory.fixed_in``) ;
5
+ - ``cvss.py`` : le score CVSS 3 d'un vecteur, et la gravité qui va avec.
6
+
7
+ Les constats qu'on en tire sont dans ``checks/vulns.py``, qui ne parle pas au réseau.
8
+ """
9
+
10
+ from forceupdate.advisories.osv import (
11
+ Advisory,
12
+ Affected,
13
+ AffectedRange,
14
+ Osv,
15
+ OsvError,
16
+ Query,
17
+ Vulnerabilities,
18
+ parse_vuln,
19
+ )
20
+
21
+ __all__ = [
22
+ "Advisory",
23
+ "Affected",
24
+ "AffectedRange",
25
+ "Osv",
26
+ "OsvError",
27
+ "Query",
28
+ "Vulnerabilities",
29
+ "parse_vuln",
30
+ ]
@@ -0,0 +1,95 @@
1
+ """Le score CVSS d'une faille, calculé depuis son vecteur, sans dépendance.
2
+
3
+ OSV donne la gravité d'une faille sous forme de vecteur (``CVSS:3.1/AV:N/AC:L/…``), pas de
4
+ score. Le calcul suit la spécification de FIRST (CVSS 3.1, section 7.1), y compris son
5
+ arrondi (annexe A), qui évite les erreurs de calcul en virgule flottante : un arrondi naïf
6
+ (``ceil(x * 10) / 10``) donne parfois un dixième de trop. Les vecteurs CVSS 3.0 ont les mêmes
7
+ formules ; seul leur arrondi diffère, et ne change le score que dans ces cas d'erreur.
8
+
9
+ CVSS 4.0 n'est pas calculé (il faut les tables de la spécification) : OSV le donne rarement
10
+ seul, et GitHub ajoute alors sa propre gravité, que lit ``advisories.osv``.
11
+
12
+ Repris de ``reference/argos_sca_parsers.py`` (``_cvss_base_score_from_vector``,
13
+ ``_cvss_to_severity``), puis adapté : ``Severity`` de ``models`` (``ARGOS (1)``), métriques
14
+ lues dans n'importe quel ordre, arrondi de la spécification, ``None`` pour un vecteur
15
+ illisible au lieu de 0.
16
+ """
17
+
18
+ import re
19
+
20
+ from forceupdate.models import Severity
21
+
22
+ _PREFIX = re.compile(r"^CVSS:3\.[01]/")
23
+ _LEADING_SCORE = re.compile(r"^(\d{1,2}\.\d)/")
24
+ """Un score écrit devant le vecteur (``9.8/CVSS:3.1/…``), comme certaines bases le font."""
25
+
26
+ _WEIGHTS: dict[str, dict[str, float]] = {
27
+ "AV": {"N": 0.85, "A": 0.62, "L": 0.55, "P": 0.2},
28
+ "AC": {"L": 0.77, "H": 0.44},
29
+ "UI": {"N": 0.85, "R": 0.62},
30
+ "C": {"H": 0.56, "L": 0.22, "N": 0.0},
31
+ "I": {"H": 0.56, "L": 0.22, "N": 0.0},
32
+ "A": {"H": 0.56, "L": 0.22, "N": 0.0},
33
+ }
34
+ _PRIVILEGES = {"N": 0.85, "L": 0.62, "H": 0.27}
35
+ _PRIVILEGES_CHANGED = {"N": 0.85, "L": 0.68, "H": 0.5}
36
+ """PR pèse plus quand la faille sort de son périmètre (``S:C``)."""
37
+
38
+
39
+ def base_score(vector: str) -> float | None:
40
+ """Le score de base (0 à 10) d'un vecteur CVSS 3.0 ou 3.1 ; ``None`` s'il est illisible.
41
+
42
+ Les métriques temporelles et environnementales (``E:``, ``RL:``…) sont ignorées : le
43
+ score de base est celui que publient les avis.
44
+ """
45
+ vector = vector.strip()
46
+ leading = _LEADING_SCORE.match(vector)
47
+ if leading:
48
+ return float(leading.group(1))
49
+ if not _PREFIX.match(vector):
50
+ return None
51
+ metrics: dict[str, str] = {}
52
+ for part in vector.split("/")[1:]:
53
+ name, _, value = part.partition(":")
54
+ metrics.setdefault(name, value)
55
+ try:
56
+ scope_changed = {"U": False, "C": True}[metrics["S"]]
57
+ privileges = (_PRIVILEGES_CHANGED if scope_changed else _PRIVILEGES)[metrics["PR"]]
58
+ weights = {name: _WEIGHTS[name][metrics[name]] for name in _WEIGHTS}
59
+ except KeyError:
60
+ return None
61
+ iss = 1 - (1 - weights["C"]) * (1 - weights["I"]) * (1 - weights["A"])
62
+ impact = (7.52 * (iss - 0.029) - 3.25 * (iss - 0.02) ** 15) if scope_changed else 6.42 * iss
63
+ if impact <= 0:
64
+ return 0.0
65
+ exploitability = 8.22 * weights["AV"] * weights["AC"] * privileges * weights["UI"]
66
+ total = impact + exploitability
67
+ if scope_changed:
68
+ total *= 1.08
69
+ return _roundup(min(total, 10.0))
70
+
71
+
72
+ def _roundup(value: float) -> float:
73
+ """Le plus petit nombre à une décimale qui est au moins ``value`` (CVSS 3.1, annexe A).
74
+
75
+ Calculé en entiers : ``ceil(4.000000000000001 * 10) / 10`` donnerait 4.1 au lieu de 4.0.
76
+ """
77
+ integer = round(value * 100_000)
78
+ if integer % 10_000 == 0:
79
+ return integer / 100_000
80
+ return (integer // 10_000 + 1) / 10
81
+
82
+
83
+ def severity(score: float) -> Severity:
84
+ """La gravité d'un score CVSS, selon l'échelle de la spécification : critique à partir de
85
+ 9, élevée à partir de 7, moyenne à partir de 4, faible au-dessus de 0. Un score nul
86
+ (aucun impact) donne ``INFO``."""
87
+ if score >= 9.0:
88
+ return Severity.CRITICAL
89
+ if score >= 7.0:
90
+ return Severity.HIGH
91
+ if score >= 4.0:
92
+ return Severity.MEDIUM
93
+ if score > 0.0:
94
+ return Severity.LOW
95
+ return Severity.INFO