java-dependency-analyzer 1.1.0__tar.gz → 1.1.1__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.
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/PKG-INFO +3 -3
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/README.md +2 -2
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/cli.py +11 -1
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/parsers/gradle_dep_tree_parser.py +160 -123
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/scanners/ghsa_scanner.py +224 -205
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/pyproject.toml +1 -1
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/LICENSE +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/__init__.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/cache/__init__.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/cache/db.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/cache/vulnerability_cache.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/models/__init__.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/models/dependency.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/models/report.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/parsers/__init__.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/parsers/base.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/parsers/gradle_parser.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/parsers/maven_dep_tree_parser.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/parsers/maven_parser.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/reporters/__init__.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/reporters/base.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/reporters/html_reporter.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/reporters/json_reporter.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/reporters/templates/report.html +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/resolvers/__init__.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/resolvers/transitive.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/scanners/__init__.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/scanners/base.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/scanners/osv_scanner.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/util/__init__.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/util/logger.py +0 -0
- {java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/logging.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: java-dependency-analyzer
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: Java Dependency Analyzer is a tool that inspects dependencies.
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -38,7 +38,7 @@ Requires-Dist: lxml (>=6.0.2,<7.0.0)
|
|
|
38
38
|
Requires-Dist: python-dotenv (>=1.2.2,<2.0.0)
|
|
39
39
|
Description-Content-Type: text/markdown
|
|
40
40
|
|
|
41
|
-
# Java Dependency Analyzer 1.1.
|
|
41
|
+
# Java Dependency Analyzer 1.1.1
|
|
42
42
|
|
|
43
43
|
> A Python CLI tool that inspects Java dependency hierarchies in Maven and Gradle projects and reports known vulnerabilities.
|
|
44
44
|
|
|
@@ -198,7 +198,7 @@ graph TD
|
|
|
198
198
|
| `GradleDepTreeParser` | `parsers/gradle_dep_tree_parser.py` | Parses `gradle dependencies` text output into a full dependency tree. |
|
|
199
199
|
| `TransitiveResolver` | `resolvers/transitive.py` | Fetches transitive dependencies by downloading POM files from Maven Central. |
|
|
200
200
|
| `OsvScanner` | `scanners/osv_scanner.py` | Queries the [OSV.dev](https://osv.dev/) batch API for known CVEs. |
|
|
201
|
-
| `GhsaScanner` | `scanners/ghsa_scanner.py` | Queries the [GitHub Advisory Database](https://github.com/advisories) REST API for security advisories. |
|
|
201
|
+
| `GhsaScanner` | `scanners/ghsa_scanner.py` | Queries the [GitHub Advisory Database](https://github.com/advisories) REST API for security advisories; automatically falls back to OSV when rate-limited (HTTP 403/429). |
|
|
202
202
|
| `VulnerabilityCache` | `cache/vulnerability_cache.py` | SQLite-backed cache for raw vulnerability API payloads with configurable TTL. |
|
|
203
203
|
| `DatabaseManager` | `cache/db.py` | Manages SQLite connection lifecycle and schema initialisation. |
|
|
204
204
|
| `JsonReporter` | `reporters/json_reporter.py` | Writes a `ScanResult` to a JSON file. |
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Java Dependency Analyzer 1.1.
|
|
1
|
+
# Java Dependency Analyzer 1.1.1
|
|
2
2
|
|
|
3
3
|
> A Python CLI tool that inspects Java dependency hierarchies in Maven and Gradle projects and reports known vulnerabilities.
|
|
4
4
|
|
|
@@ -158,7 +158,7 @@ graph TD
|
|
|
158
158
|
| `GradleDepTreeParser` | `parsers/gradle_dep_tree_parser.py` | Parses `gradle dependencies` text output into a full dependency tree. |
|
|
159
159
|
| `TransitiveResolver` | `resolvers/transitive.py` | Fetches transitive dependencies by downloading POM files from Maven Central. |
|
|
160
160
|
| `OsvScanner` | `scanners/osv_scanner.py` | Queries the [OSV.dev](https://osv.dev/) batch API for known CVEs. |
|
|
161
|
-
| `GhsaScanner` | `scanners/ghsa_scanner.py` | Queries the [GitHub Advisory Database](https://github.com/advisories) REST API for security advisories. |
|
|
161
|
+
| `GhsaScanner` | `scanners/ghsa_scanner.py` | Queries the [GitHub Advisory Database](https://github.com/advisories) REST API for security advisories; automatically falls back to OSV when rate-limited (HTTP 403/429). |
|
|
162
162
|
| `VulnerabilityCache` | `cache/vulnerability_cache.py` | SQLite-backed cache for raw vulnerability API payloads with configurable TTL. |
|
|
163
163
|
| `DatabaseManager` | `cache/db.py` | Manages SQLite connection lifecycle and schema initialisation. |
|
|
164
164
|
| `JsonReporter` | `reporters/json_reporter.py` | Writes a `ScanResult` to a JSON file. |
|
{java_dependency_analyzer-1.1.0 → java_dependency_analyzer-1.1.1}/java_dependency_analyzer/cli.py
RENAMED
|
@@ -381,7 +381,17 @@ def _scan_all(
|
|
|
381
381
|
for dep in dependencies:
|
|
382
382
|
if verbose:
|
|
383
383
|
click.echo(f" Scanning {dep.coordinates}...")
|
|
384
|
-
|
|
384
|
+
if not ghsa.rate_limited:
|
|
385
|
+
ghsa_vulns = ghsa.scan(dep)
|
|
386
|
+
if ghsa.rate_limited:
|
|
387
|
+
click.echo(
|
|
388
|
+
" GHSA rate limit exceeded; "
|
|
389
|
+
"falling back to OSV for remaining dependencies.",
|
|
390
|
+
err=True,
|
|
391
|
+
)
|
|
392
|
+
ghsa_vulns = []
|
|
393
|
+
else:
|
|
394
|
+
ghsa_vulns = []
|
|
385
395
|
dep.vulnerabilities = ghsa_vulns if ghsa_vulns else osv.scan(dep)
|
|
386
396
|
_scan_all(dep.transitive_dependencies, osv, ghsa, verbose)
|
|
387
397
|
|
|
@@ -1,123 +1,160 @@
|
|
|
1
|
-
"""
|
|
2
|
-
gradle_dep_tree_parser module.
|
|
3
|
-
|
|
4
|
-
Parses the text output of ``gradle dependencies`` to reconstruct
|
|
5
|
-
the full dependency tree including transitive dependencies.
|
|
6
|
-
|
|
7
|
-
:author: Ron Webb
|
|
8
|
-
:since: 1.0.0
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import re
|
|
12
|
-
|
|
13
|
-
from ..models.dependency import Dependency
|
|
14
|
-
from ..util.logger import setup_logger
|
|
15
|
-
from .base import DepTreeParser
|
|
16
|
-
|
|
17
|
-
__author__ = "Ron Webb"
|
|
18
|
-
__since__ = "1.0.0"
|
|
19
|
-
|
|
20
|
-
_logger = setup_logger(__name__)
|
|
21
|
-
|
|
22
|
-
# Matches the tree connector characters at the start of a dependency line.
|
|
23
|
-
# Each level of indentation is exactly 5 characters wide (" " or "| ").
|
|
24
|
-
_INDENT_UNIT = 5
|
|
25
|
-
_CONNECTOR_RE = re.compile(r"^((?:[|\\+]\s{3,4}|\s{5})*)([+\\])--- (.+)$")
|
|
26
|
-
|
|
27
|
-
# Matches a resolved version arrow, e.g. "1.0 -> 2.0" or "
|
|
28
|
-
_VERSION_ARROW_RE = re.compile(r"\s*->\s*(\S+)$")
|
|
29
|
-
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
1
|
+
"""
|
|
2
|
+
gradle_dep_tree_parser module.
|
|
3
|
+
|
|
4
|
+
Parses the text output of ``gradle dependencies`` to reconstruct
|
|
5
|
+
the full dependency tree including transitive dependencies.
|
|
6
|
+
|
|
7
|
+
:author: Ron Webb
|
|
8
|
+
:since: 1.0.0
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
from ..models.dependency import Dependency
|
|
14
|
+
from ..util.logger import setup_logger
|
|
15
|
+
from .base import DepTreeParser
|
|
16
|
+
|
|
17
|
+
__author__ = "Ron Webb"
|
|
18
|
+
__since__ = "1.0.0"
|
|
19
|
+
|
|
20
|
+
_logger = setup_logger(__name__)
|
|
21
|
+
|
|
22
|
+
# Matches the tree connector characters at the start of a dependency line.
|
|
23
|
+
# Each level of indentation is exactly 5 characters wide (" " or "| ").
|
|
24
|
+
_INDENT_UNIT = 5
|
|
25
|
+
_CONNECTOR_RE = re.compile(r"^((?:[|\\+]\s{3,4}|\s{5})*)([+\\])--- (.+)$")
|
|
26
|
+
|
|
27
|
+
# Matches a resolved version arrow, e.g. "1.0 -> 2.0", "RELEASE -> 4.16.0", or "artifact -> 4.16.0"
|
|
28
|
+
_VERSION_ARROW_RE = re.compile(r"\s*->\s*(\S+)$")
|
|
29
|
+
|
|
30
|
+
# Gradle coordinates without an explicit version: group:artifact (version comes from arrow)
|
|
31
|
+
_COORD_NO_VERSION_RE = re.compile(r"^([^:]+):([^:]+)$")
|
|
32
|
+
|
|
33
|
+
# Suffix appended to repeated subtree roots by Gradle
|
|
34
|
+
_REPEATED_SUFFIX = " (*)"
|
|
35
|
+
|
|
36
|
+
# Lines annotated as constraints (not actual dependencies)
|
|
37
|
+
_CONSTRAINT_SUFFIX = " (c)"
|
|
38
|
+
|
|
39
|
+
# Gradle coordinates: group:artifact:version
|
|
40
|
+
_COORD_RE = re.compile(r"^([^:]+):([^:]+):(.+)$")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class GradleDepTreeParser(DepTreeParser):
|
|
44
|
+
"""
|
|
45
|
+
Parses the plain-text output of ``gradle dependencies`` and reconstructs
|
|
46
|
+
the dependency tree as a list of :class:`~java_dependency_analyzer.models.dependency.Dependency`
|
|
47
|
+
objects with nested ``transitive_dependencies``.
|
|
48
|
+
|
|
49
|
+
The parser is format-agnostic regarding the configuration name; it processes
|
|
50
|
+
the first dependency-tree block it encounters. Users should redirect the
|
|
51
|
+
output of the configuration they care about (e.g. ``runtimeClasspath``) into
|
|
52
|
+
a text file and pass that file here.
|
|
53
|
+
|
|
54
|
+
:author: Ron Webb
|
|
55
|
+
:since: 1.0.0
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Initialise the parser with an empty version-resolution cache.
|
|
61
|
+
|
|
62
|
+
:author: Ron Webb
|
|
63
|
+
:since: 1.1.1
|
|
64
|
+
"""
|
|
65
|
+
# Maps (group_id, artifact_id) -> resolved version from a -> arrow
|
|
66
|
+
self._resolutions: dict[tuple[str, str], str] = {}
|
|
67
|
+
|
|
68
|
+
def parse(self, file_path: str) -> list[Dependency]:
|
|
69
|
+
"""
|
|
70
|
+
Reset the resolution cache and delegate to the base parser.
|
|
71
|
+
|
|
72
|
+
:author: Ron Webb
|
|
73
|
+
:since: 1.1.1
|
|
74
|
+
"""
|
|
75
|
+
self._resolutions = {}
|
|
76
|
+
return super().parse(file_path)
|
|
77
|
+
|
|
78
|
+
# ------------------------------------------------------------------
|
|
79
|
+
# Private helpers
|
|
80
|
+
# ------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
def _line_to_entry(self, line: str) -> tuple[int, bool, Dependency] | None:
|
|
83
|
+
"""
|
|
84
|
+
Convert a single Gradle dep-tree line to a ``(depth, is_leaf, dep)``
|
|
85
|
+
entry, or return *None* to skip the line.
|
|
86
|
+
|
|
87
|
+
:author: Ron Webb
|
|
88
|
+
:since: 1.0.0
|
|
89
|
+
"""
|
|
90
|
+
if line.rstrip().endswith(_CONSTRAINT_SUFFIX):
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
match = _CONNECTOR_RE.match(line)
|
|
94
|
+
if match is None:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
indent_str, coord_str = match.group(1), match.group(3)
|
|
98
|
+
depth = len(indent_str) // _INDENT_UNIT
|
|
99
|
+
|
|
100
|
+
is_leaf = coord_str.endswith(_REPEATED_SUFFIX)
|
|
101
|
+
if is_leaf:
|
|
102
|
+
coord_str = coord_str[: -len(_REPEATED_SUFFIX)]
|
|
103
|
+
|
|
104
|
+
dep = self._parse_coordinate(coord_str, depth)
|
|
105
|
+
if dep is None:
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
return depth, is_leaf, dep
|
|
109
|
+
|
|
110
|
+
def _parse_coordinate(self, coord_str: str, depth: int) -> Dependency | None:
|
|
111
|
+
"""
|
|
112
|
+
Convert a Gradle coordinate string (with optional ``->`` resolution)
|
|
113
|
+
into a :class:`Dependency`. Returns *None* if the string cannot be
|
|
114
|
+
parsed.
|
|
115
|
+
|
|
116
|
+
:author: Ron Webb
|
|
117
|
+
:since: 1.0.0
|
|
118
|
+
"""
|
|
119
|
+
# Resolve version arrows: "group:artifact:1.0 -> 2.0" or "group:artifact -> 2.0"
|
|
120
|
+
arrow_match = _VERSION_ARROW_RE.search(coord_str)
|
|
121
|
+
if arrow_match:
|
|
122
|
+
resolved_version = arrow_match.group(1)
|
|
123
|
+
# Strip the arrow portion from the coordinate
|
|
124
|
+
coord_str = coord_str[: arrow_match.start()]
|
|
125
|
+
else:
|
|
126
|
+
resolved_version = None
|
|
127
|
+
|
|
128
|
+
coord_str = coord_str.strip()
|
|
129
|
+
coord_match = _COORD_RE.match(coord_str)
|
|
130
|
+
if coord_match is not None:
|
|
131
|
+
group_id = coord_match.group(1).strip()
|
|
132
|
+
artifact_id = coord_match.group(2).strip()
|
|
133
|
+
version = resolved_version or coord_match.group(3).strip()
|
|
134
|
+
else:
|
|
135
|
+
# Handle "group:artifact -> version" (no version before the arrow)
|
|
136
|
+
no_ver_match = _COORD_NO_VERSION_RE.match(coord_str)
|
|
137
|
+
if no_ver_match and resolved_version:
|
|
138
|
+
group_id = no_ver_match.group(1).strip()
|
|
139
|
+
artifact_id = no_ver_match.group(2).strip()
|
|
140
|
+
version = resolved_version
|
|
141
|
+
else:
|
|
142
|
+
_logger.debug("Could not parse coordinate: %s", coord_str)
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
if not group_id or not artifact_id or not version:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
# Store the resolved version the first time a -> arrow is seen for this artifact
|
|
149
|
+
if resolved_version:
|
|
150
|
+
self._resolutions[(group_id, artifact_id)] = resolved_version
|
|
151
|
+
# Apply any cached resolution so (*) repeated entries use the resolved version
|
|
152
|
+
version = self._resolutions.get((group_id, artifact_id), version)
|
|
153
|
+
|
|
154
|
+
return Dependency(
|
|
155
|
+
group_id=group_id,
|
|
156
|
+
artifact_id=artifact_id,
|
|
157
|
+
version=version,
|
|
158
|
+
scope="runtime",
|
|
159
|
+
depth=depth,
|
|
160
|
+
)
|
|
@@ -1,205 +1,224 @@
|
|
|
1
|
-
"""
|
|
2
|
-
ghsa_scanner module.
|
|
3
|
-
|
|
4
|
-
Queries the GitHub Advisory Database REST API to get vulnerability information
|
|
5
|
-
for Maven packages.
|
|
6
|
-
|
|
7
|
-
:author: Ron Webb
|
|
8
|
-
:since: 1.0.0
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import json
|
|
12
|
-
import os
|
|
13
|
-
|
|
14
|
-
import httpx
|
|
15
|
-
from dotenv import load_dotenv
|
|
16
|
-
|
|
17
|
-
from ..cache.vulnerability_cache import VulnerabilityCache
|
|
18
|
-
from ..models.dependency import Dependency, Vulnerability
|
|
19
|
-
from ..util.logger import setup_logger
|
|
20
|
-
from .base import VulnerabilityScanner
|
|
21
|
-
|
|
22
|
-
__author__ = "Ron Webb"
|
|
23
|
-
__since__ = "1.0.0"
|
|
24
|
-
|
|
25
|
-
load_dotenv()
|
|
26
|
-
|
|
27
|
-
_logger = setup_logger(__name__)
|
|
28
|
-
|
|
29
|
-
_GHSA_API_URL = "https://api.github.com/advisories"
|
|
30
|
-
_ACCEPT_HEADER = "application/vnd.github+json"
|
|
31
|
-
_API_VERSION_HEADER = "2022-11-28"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class GhsaScanner(VulnerabilityScanner):
|
|
35
|
-
"""
|
|
36
|
-
Queries the GitHub Advisory Database REST API (https://api.github.com/advisories)
|
|
37
|
-
to find reviewed security advisories for a given Maven dependency version.
|
|
38
|
-
|
|
39
|
-
Supports optional authentication via the ``GITHUB_TOKEN`` environment variable to
|
|
40
|
-
increase the API rate limit from 60 to 5000 requests per hour.
|
|
41
|
-
|
|
42
|
-
:author: Ron Webb
|
|
43
|
-
:since: 1.0.0
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
def __init__(
|
|
47
|
-
self,
|
|
48
|
-
client: httpx.Client | None = None,
|
|
49
|
-
cache: VulnerabilityCache | None = None,
|
|
50
|
-
) -> None:
|
|
51
|
-
"""
|
|
52
|
-
Initialise the scanner with an optional shared httpx client and cache.
|
|
53
|
-
|
|
54
|
-
If ``GITHUB_TOKEN`` is set in the environment, it is forwarded as a
|
|
55
|
-
``Bearer`` token to raise the GitHub API rate limit.
|
|
56
|
-
When *cache* is provided, scan results are read from and written to it.
|
|
57
|
-
|
|
58
|
-
:author: Ron Webb
|
|
59
|
-
:since: 1.0.0
|
|
60
|
-
"""
|
|
61
|
-
headers = {
|
|
62
|
-
"Accept": _ACCEPT_HEADER,
|
|
63
|
-
"X-GitHub-Api-Version": _API_VERSION_HEADER,
|
|
64
|
-
}
|
|
65
|
-
token = os.environ.get("GITHUB_TOKEN")
|
|
66
|
-
if token:
|
|
67
|
-
headers["Authorization"] = f"Bearer {token}"
|
|
68
|
-
|
|
69
|
-
self._client = client or httpx.Client(timeout=30, headers=headers)
|
|
70
|
-
self._cache = cache
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
1
|
+
"""
|
|
2
|
+
ghsa_scanner module.
|
|
3
|
+
|
|
4
|
+
Queries the GitHub Advisory Database REST API to get vulnerability information
|
|
5
|
+
for Maven packages.
|
|
6
|
+
|
|
7
|
+
:author: Ron Webb
|
|
8
|
+
:since: 1.0.0
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
from dotenv import load_dotenv
|
|
16
|
+
|
|
17
|
+
from ..cache.vulnerability_cache import VulnerabilityCache
|
|
18
|
+
from ..models.dependency import Dependency, Vulnerability
|
|
19
|
+
from ..util.logger import setup_logger
|
|
20
|
+
from .base import VulnerabilityScanner
|
|
21
|
+
|
|
22
|
+
__author__ = "Ron Webb"
|
|
23
|
+
__since__ = "1.0.0"
|
|
24
|
+
|
|
25
|
+
load_dotenv()
|
|
26
|
+
|
|
27
|
+
_logger = setup_logger(__name__)
|
|
28
|
+
|
|
29
|
+
_GHSA_API_URL = "https://api.github.com/advisories"
|
|
30
|
+
_ACCEPT_HEADER = "application/vnd.github+json"
|
|
31
|
+
_API_VERSION_HEADER = "2022-11-28"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class GhsaScanner(VulnerabilityScanner):
|
|
35
|
+
"""
|
|
36
|
+
Queries the GitHub Advisory Database REST API (https://api.github.com/advisories)
|
|
37
|
+
to find reviewed security advisories for a given Maven dependency version.
|
|
38
|
+
|
|
39
|
+
Supports optional authentication via the ``GITHUB_TOKEN`` environment variable to
|
|
40
|
+
increase the API rate limit from 60 to 5000 requests per hour.
|
|
41
|
+
|
|
42
|
+
:author: Ron Webb
|
|
43
|
+
:since: 1.0.0
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
client: httpx.Client | None = None,
|
|
49
|
+
cache: VulnerabilityCache | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""
|
|
52
|
+
Initialise the scanner with an optional shared httpx client and cache.
|
|
53
|
+
|
|
54
|
+
If ``GITHUB_TOKEN`` is set in the environment, it is forwarded as a
|
|
55
|
+
``Bearer`` token to raise the GitHub API rate limit.
|
|
56
|
+
When *cache* is provided, scan results are read from and written to it.
|
|
57
|
+
|
|
58
|
+
:author: Ron Webb
|
|
59
|
+
:since: 1.0.0
|
|
60
|
+
"""
|
|
61
|
+
headers = {
|
|
62
|
+
"Accept": _ACCEPT_HEADER,
|
|
63
|
+
"X-GitHub-Api-Version": _API_VERSION_HEADER,
|
|
64
|
+
}
|
|
65
|
+
token = os.environ.get("GITHUB_TOKEN")
|
|
66
|
+
if token:
|
|
67
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
68
|
+
|
|
69
|
+
self._client = client or httpx.Client(timeout=30, headers=headers)
|
|
70
|
+
self._cache = cache
|
|
71
|
+
self._rate_limited: bool = False
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def rate_limited(self) -> bool:
|
|
75
|
+
"""
|
|
76
|
+
Return ``True`` once the GitHub Advisory API has responded with a
|
|
77
|
+
rate-limit error (HTTP 403 or 429) so callers can skip further requests.
|
|
78
|
+
|
|
79
|
+
:author: Ron Webb
|
|
80
|
+
:since: 1.1.1
|
|
81
|
+
"""
|
|
82
|
+
return self._rate_limited
|
|
83
|
+
|
|
84
|
+
def scan(self, dependency: Dependency) -> list[Vulnerability]:
|
|
85
|
+
"""
|
|
86
|
+
Query the GitHub Advisory Database for advisories affecting this dependency.
|
|
87
|
+
|
|
88
|
+
Checks the cache first when one is configured; only calls the API on a
|
|
89
|
+
cache miss and stores the raw response on success.
|
|
90
|
+
|
|
91
|
+
Uses the ``affects`` query parameter with ``group:artifact@version`` notation
|
|
92
|
+
and filters by ``ecosystem=maven`` and ``type=reviewed``.
|
|
93
|
+
|
|
94
|
+
:author: Ron Webb
|
|
95
|
+
:since: 1.0.0
|
|
96
|
+
"""
|
|
97
|
+
_logger.debug("Querying GHSA for %s", dependency.coordinates)
|
|
98
|
+
cached = self._get_cached("ghsa", dependency)
|
|
99
|
+
if cached is not None:
|
|
100
|
+
return self._apply_cache_source(cached, "ghsa")
|
|
101
|
+
|
|
102
|
+
affects = f"{dependency.group_id}:{dependency.artifact_id}@{dependency.version}"
|
|
103
|
+
params = {
|
|
104
|
+
"ecosystem": "maven",
|
|
105
|
+
"affects": affects,
|
|
106
|
+
"type": "reviewed",
|
|
107
|
+
}
|
|
108
|
+
if self._rate_limited:
|
|
109
|
+
return []
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
response = self._client.get(_GHSA_API_URL, params=params)
|
|
113
|
+
if response.status_code in (429, 403) and (
|
|
114
|
+
response.status_code == 429 or "rate limit" in response.text.lower()
|
|
115
|
+
):
|
|
116
|
+
_logger.warning(
|
|
117
|
+
"GitHub Advisory API rate limit exceeded (HTTP %s); "
|
|
118
|
+
"disabling GHSA for this run",
|
|
119
|
+
response.status_code,
|
|
120
|
+
)
|
|
121
|
+
self._rate_limited = True
|
|
122
|
+
return []
|
|
123
|
+
response.raise_for_status()
|
|
124
|
+
data = response.json()
|
|
125
|
+
except httpx.HTTPError as exc:
|
|
126
|
+
_logger.warning("GHSA query failed for %s: %s", dependency.coordinates, exc)
|
|
127
|
+
return []
|
|
128
|
+
|
|
129
|
+
self._put_cached("ghsa", dependency, json.dumps(data))
|
|
130
|
+
return self._parse_response(data)
|
|
131
|
+
|
|
132
|
+
def _parse_response(self, data: list) -> list[Vulnerability]:
|
|
133
|
+
"""
|
|
134
|
+
Parse the GitHub Advisory API response (a JSON array) into Vulnerability objects.
|
|
135
|
+
|
|
136
|
+
:author: Ron Webb
|
|
137
|
+
:since: 1.0.0
|
|
138
|
+
"""
|
|
139
|
+
vulns: list[Vulnerability] = []
|
|
140
|
+
for advisory in data:
|
|
141
|
+
vuln_obj = self._parse_advisory(advisory)
|
|
142
|
+
if vuln_obj is not None:
|
|
143
|
+
vulns.append(vuln_obj)
|
|
144
|
+
return vulns
|
|
145
|
+
|
|
146
|
+
def _parse_advisory(self, advisory: dict) -> Vulnerability | None:
|
|
147
|
+
"""
|
|
148
|
+
Convert a single GitHub Advisory dict into a Vulnerability object.
|
|
149
|
+
|
|
150
|
+
The ``cve_id`` field prefers the CVE identifier when available and falls
|
|
151
|
+
back to the GHSA identifier so every returned advisory has a unique ID.
|
|
152
|
+
|
|
153
|
+
:author: Ron Webb
|
|
154
|
+
:since: 1.0.0
|
|
155
|
+
"""
|
|
156
|
+
ghsa_id = advisory.get("ghsa_id", "UNKNOWN")
|
|
157
|
+
cve_id = advisory.get("cve_id") or ghsa_id
|
|
158
|
+
summary = advisory.get("summary", "No summary available")
|
|
159
|
+
severity = self._extract_severity(advisory)
|
|
160
|
+
affected_versions = self._extract_affected_versions(advisory)
|
|
161
|
+
reference_url = advisory.get("html_url", "")
|
|
162
|
+
|
|
163
|
+
return Vulnerability(
|
|
164
|
+
cve_id=cve_id,
|
|
165
|
+
summary=summary,
|
|
166
|
+
severity=severity,
|
|
167
|
+
affected_versions=affected_versions,
|
|
168
|
+
source="ghsa",
|
|
169
|
+
reference_url=reference_url,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def _extract_severity(self, advisory: dict) -> str:
|
|
173
|
+
"""
|
|
174
|
+
Extract a human-readable severity string from a GitHub Advisory entry.
|
|
175
|
+
|
|
176
|
+
Tries, in order:
|
|
177
|
+
1. The top-level ``severity`` label (e.g. ``"high"``, ``"critical"``).
|
|
178
|
+
2. The CVSS v4 score from ``cvss_severities.cvss_v4.score``.
|
|
179
|
+
3. The CVSS v3 score from ``cvss_severities.cvss_v3.score``.
|
|
180
|
+
4. The legacy ``cvss.score`` field.
|
|
181
|
+
|
|
182
|
+
:author: Ron Webb
|
|
183
|
+
:since: 1.0.0
|
|
184
|
+
"""
|
|
185
|
+
label = advisory.get("severity")
|
|
186
|
+
if label and label not in ("", "unknown"):
|
|
187
|
+
return label.upper()
|
|
188
|
+
|
|
189
|
+
cvss_severities = advisory.get("cvss_severities", {})
|
|
190
|
+
for key in ("cvss_v4", "cvss_v3"):
|
|
191
|
+
score = (cvss_severities.get(key) or {}).get("score")
|
|
192
|
+
if score is not None:
|
|
193
|
+
return str(score)
|
|
194
|
+
|
|
195
|
+
legacy_score = (advisory.get("cvss") or {}).get("score")
|
|
196
|
+
if legacy_score is not None:
|
|
197
|
+
return str(legacy_score)
|
|
198
|
+
|
|
199
|
+
return "UNKNOWN"
|
|
200
|
+
|
|
201
|
+
def _extract_affected_versions(self, advisory: dict) -> list[str]:
|
|
202
|
+
"""
|
|
203
|
+
Extract affected version range strings from a GitHub Advisory entry.
|
|
204
|
+
|
|
205
|
+
Each entry in ``vulnerabilities`` may carry a ``vulnerable_version_range``
|
|
206
|
+
string (e.g. ``"< 2.17.0"`` or ``">= 2.0.0, < 2.15.0"``). The method
|
|
207
|
+
splits compound ranges on commas so the returned list contains individual
|
|
208
|
+
constraint tokens.
|
|
209
|
+
|
|
210
|
+
:author: Ron Webb
|
|
211
|
+
:since: 1.0.0
|
|
212
|
+
"""
|
|
213
|
+
affected_versions: list[str] = []
|
|
214
|
+
seen: set[str] = set()
|
|
215
|
+
for vuln_entry in advisory.get("vulnerabilities", []):
|
|
216
|
+
version_range = vuln_entry.get("vulnerable_version_range")
|
|
217
|
+
if not version_range:
|
|
218
|
+
continue
|
|
219
|
+
for part in version_range.split(","):
|
|
220
|
+
constraint = part.strip()
|
|
221
|
+
if constraint and constraint not in seen:
|
|
222
|
+
affected_versions.append(constraint)
|
|
223
|
+
seen.add(constraint)
|
|
224
|
+
return affected_versions
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|