java-dependency-analyzer 1.0.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 (31) hide show
  1. java_dependency_analyzer/__init__.py +11 -0
  2. java_dependency_analyzer/cache/__init__.py +11 -0
  3. java_dependency_analyzer/cache/db.py +101 -0
  4. java_dependency_analyzer/cache/vulnerability_cache.py +156 -0
  5. java_dependency_analyzer/cli.py +394 -0
  6. java_dependency_analyzer/models/__init__.py +11 -0
  7. java_dependency_analyzer/models/dependency.py +80 -0
  8. java_dependency_analyzer/models/report.py +108 -0
  9. java_dependency_analyzer/parsers/__init__.py +11 -0
  10. java_dependency_analyzer/parsers/base.py +150 -0
  11. java_dependency_analyzer/parsers/gradle_dep_tree_parser.py +125 -0
  12. java_dependency_analyzer/parsers/gradle_parser.py +206 -0
  13. java_dependency_analyzer/parsers/maven_dep_tree_parser.py +123 -0
  14. java_dependency_analyzer/parsers/maven_parser.py +182 -0
  15. java_dependency_analyzer/reporters/__init__.py +11 -0
  16. java_dependency_analyzer/reporters/base.py +33 -0
  17. java_dependency_analyzer/reporters/html_reporter.py +82 -0
  18. java_dependency_analyzer/reporters/json_reporter.py +52 -0
  19. java_dependency_analyzer/reporters/templates/report.html +406 -0
  20. java_dependency_analyzer/resolvers/__init__.py +11 -0
  21. java_dependency_analyzer/resolvers/transitive.py +276 -0
  22. java_dependency_analyzer/scanners/__init__.py +11 -0
  23. java_dependency_analyzer/scanners/base.py +102 -0
  24. java_dependency_analyzer/scanners/ghsa_scanner.py +204 -0
  25. java_dependency_analyzer/scanners/osv_scanner.py +167 -0
  26. java_dependency_analyzer/util/__init__.py +11 -0
  27. java_dependency_analyzer/util/logger.py +48 -0
  28. java_dependency_analyzer-1.0.0.dist-info/METADATA +193 -0
  29. java_dependency_analyzer-1.0.0.dist-info/RECORD +31 -0
  30. java_dependency_analyzer-1.0.0.dist-info/WHEEL +4 -0
  31. java_dependency_analyzer-1.0.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,123 @@
1
+ """
2
+ maven_dep_tree_parser module.
3
+
4
+ Parses the text output of ``mvn dependency:tree`` 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
+ # Maven tree output prefix on every INFO line
23
+ _INFO_PREFIX = "[INFO] "
24
+ _INFO_PREFIX_LEN = len(_INFO_PREFIX)
25
+
26
+ # Each level of indentation is 3 characters wide ("| " or " ")
27
+ _INDENT_UNIT = 3
28
+
29
+ # Matches the tree connector at the start of a dependency after stripping [INFO]
30
+ # Groups: (1) indent text, (2) connector "+- " or "\- ", (3) coordinate string
31
+ _CONNECTOR_RE = re.compile(r"^((?:[|\\+]\s{2}|\s{3})*)[+\\]- (.+)$")
32
+
33
+ # Matches Maven coordinates: group:artifact:type:version:scope
34
+ _COORD_RE = re.compile(r"^([^:]+):([^:]+):[^:]+:([^:]+):([^:]+)$")
35
+
36
+
37
+ class MavenDepTreeParser(DepTreeParser):
38
+ """
39
+ Parses the plain-text output of ``mvn dependency:tree`` and reconstructs
40
+ the dependency tree as a list of :class:`~java_dependency_analyzer.models.dependency.Dependency`
41
+ objects with nested ``transitive_dependencies``.
42
+
43
+ Only lines with a ``+- `` or ``\\- `` connector are interpreted as
44
+ dependency entries; all other lines (headers, the root artifact line,
45
+ ``[INFO] BUILD SUCCESS``, etc.) are silently skipped.
46
+
47
+ :author: Ron Webb
48
+ :since: 1.0.0
49
+ """
50
+
51
+ # ------------------------------------------------------------------
52
+ # Private helpers
53
+ # ------------------------------------------------------------------
54
+
55
+ def _line_to_entry(
56
+ self, line: str
57
+ ) -> tuple[int, bool, Dependency] | None:
58
+ """
59
+ Convert a single Maven dep-tree line to a ``(depth, is_leaf, dep)``
60
+ entry, or return *None* to skip the line.
61
+
62
+ :author: Ron Webb
63
+ :since: 1.0.0
64
+ """
65
+ stripped = self._strip_info_prefix(line)
66
+ if stripped is None:
67
+ return None
68
+
69
+ match = _CONNECTOR_RE.match(stripped)
70
+ if match is None:
71
+ return None
72
+
73
+ indent_str = match.group(1)
74
+ coord_str = match.group(2)
75
+ depth = len(indent_str) // _INDENT_UNIT
76
+
77
+ dep = self._parse_coordinate(coord_str, depth)
78
+ if dep is None:
79
+ return None
80
+
81
+ return depth, False, dep
82
+
83
+ def _strip_info_prefix(self, line: str) -> str | None:
84
+ """
85
+ Remove the ``[INFO] `` prefix from a Maven log line; return *None*
86
+ if the prefix is absent (i.e. the line is not a Maven INFO line).
87
+
88
+ :author: Ron Webb
89
+ :since: 1.0.0
90
+ """
91
+ if line.startswith(_INFO_PREFIX):
92
+ return line[_INFO_PREFIX_LEN:]
93
+ return None
94
+
95
+ def _parse_coordinate(self, coord_str: str, depth: int) -> Dependency | None:
96
+ """
97
+ Convert a Maven coordinate string (``group:artifact:type:version:scope``)
98
+ into a :class:`Dependency`. Returns *None* if the string cannot be
99
+ parsed.
100
+
101
+ :author: Ron Webb
102
+ :since: 1.0.0
103
+ """
104
+ coord_match = _COORD_RE.match(coord_str.strip())
105
+ if coord_match is None:
106
+ _logger.debug("Could not parse Maven coordinate: %s", coord_str)
107
+ return None
108
+
109
+ group_id = coord_match.group(1).strip()
110
+ artifact_id = coord_match.group(2).strip()
111
+ version = coord_match.group(3).strip()
112
+ scope = coord_match.group(4).strip()
113
+
114
+ if not group_id or not artifact_id or not version:
115
+ return None
116
+
117
+ return Dependency(
118
+ group_id=group_id,
119
+ artifact_id=artifact_id,
120
+ version=version,
121
+ scope=scope,
122
+ depth=depth,
123
+ )
@@ -0,0 +1,182 @@
1
+ """
2
+ maven_parser module.
3
+
4
+ Parses Maven pom.xml files to extract runtime dependencies.
5
+
6
+ :author: Ron Webb
7
+ :since: 1.0.0
8
+ """
9
+
10
+ import re
11
+ from lxml import etree
12
+
13
+ from ..models.dependency import Dependency
14
+ from ..util.logger import setup_logger
15
+ from .base import DependencyParser, RUNTIME_SCOPES
16
+
17
+ __author__ = "Ron Webb"
18
+ __since__ = "1.0.0"
19
+
20
+ _logger = setup_logger(__name__)
21
+
22
+ # POM XML namespace
23
+ _POM_NS = "http://maven.apache.org/POM/4.0.0"
24
+
25
+
26
+ class MavenParser(DependencyParser):
27
+ """
28
+ Parses a Maven pom.xml file and extracts runtime dependencies.
29
+
30
+ Handles property placeholder substitution (e.g., ${project.version})
31
+ and filters to compile/runtime scopes only.
32
+
33
+ :author: Ron Webb
34
+ :since: 1.0.0
35
+ """
36
+
37
+ def parse(self, file_path: str) -> list[Dependency]:
38
+ """
39
+ Parse the given pom.xml file and return a list of direct dependencies.
40
+
41
+ :author: Ron Webb
42
+ :since: 1.0.0
43
+ """
44
+ _logger.info("Parsing Maven POM: %s", file_path)
45
+ try:
46
+ tree = etree.parse(file_path) # nosec B320 # pylint: disable=c-extension-no-member
47
+ except etree.XMLSyntaxError as exc: # pylint: disable=c-extension-no-member
48
+ _logger.error("Failed to parse POM XML: %s", exc)
49
+ return []
50
+
51
+ root = tree.getroot()
52
+ namespace = self._detect_namespace(root)
53
+ properties = self._extract_properties(root, namespace)
54
+ return self._extract_dependencies(root, namespace, properties)
55
+
56
+ def _detect_namespace(self, root: etree._Element) -> dict[str, str]:
57
+ """
58
+ Detect whether the POM uses the standard Maven namespace or none.
59
+
60
+ :author: Ron Webb
61
+ :since: 1.0.0
62
+ """
63
+ if root.tag.startswith("{"):
64
+ return {"m": _POM_NS}
65
+ return {}
66
+
67
+ def _tag(self, name: str, namespace: dict[str, str]) -> str:
68
+ """
69
+ Return the namespaced or plain tag name for an element lookup.
70
+
71
+ :author: Ron Webb
72
+ :since: 1.0.0
73
+ """
74
+ if namespace:
75
+ return f"{{{_POM_NS}}}{name}"
76
+ return name
77
+
78
+ def _extract_properties(
79
+ self, root: etree._Element, namespace: dict[str, str]
80
+ ) -> dict[str, str]:
81
+ """
82
+ Extract all <properties> entries from the POM for variable substitution.
83
+
84
+ :author: Ron Webb
85
+ :since: 1.0.0
86
+ """
87
+ props: dict[str, str] = {}
88
+ props_el = root.find(self._tag("properties", namespace))
89
+ if props_el is not None:
90
+ for child in props_el:
91
+ local = etree.QName(child.tag).localname # pylint: disable=c-extension-no-member
92
+ if child.text:
93
+ props[local] = child.text.strip()
94
+
95
+ # Add standard project coordinates as substitutable properties
96
+ for coord in ("groupId", "artifactId", "version"):
97
+ coord_el = root.find(self._tag(coord, namespace))
98
+ if coord_el is not None and coord_el.text:
99
+ props[f"project.{coord}"] = coord_el.text.strip()
100
+ props[coord] = coord_el.text.strip()
101
+
102
+ return props
103
+
104
+ def _resolve_value(self, value: str, properties: dict[str, str]) -> str:
105
+ """
106
+ Replace ${property} placeholders with values from the properties map.
107
+
108
+ :author: Ron Webb
109
+ :since: 1.0.0
110
+ """
111
+ pattern = re.compile(r"\$\{([^}]+)\}")
112
+ for match in pattern.finditer(value):
113
+ key = match.group(1)
114
+ replacement = properties.get(key, "")
115
+ value = value.replace(match.group(0), replacement)
116
+ return value
117
+
118
+ def _extract_dependencies(
119
+ self,
120
+ root: etree._Element,
121
+ namespace: dict[str, str],
122
+ properties: dict[str, str],
123
+ ) -> list[Dependency]:
124
+ """
125
+ Extract <dependency> elements, filter by scope, and return Dependency objects.
126
+
127
+ :author: Ron Webb
128
+ :since: 1.0.0
129
+ """
130
+ deps: list[Dependency] = []
131
+ deps_container = root.find(self._tag("dependencies", namespace))
132
+ if deps_container is None:
133
+ return deps
134
+
135
+ for dep_el in deps_container.findall(self._tag("dependency", namespace)):
136
+ dep = self._parse_dependency_element(dep_el, namespace, properties)
137
+ if dep is not None:
138
+ deps.append(dep)
139
+
140
+ return deps
141
+
142
+ def _parse_dependency_element(
143
+ self,
144
+ dep_el: etree._Element,
145
+ namespace: dict[str, str],
146
+ properties: dict[str, str],
147
+ ) -> Dependency | None:
148
+ """
149
+ Parse a single <dependency> element into a Dependency object.
150
+
151
+ Returns None if the dependency should be skipped (wrong scope, missing fields).
152
+
153
+ :author: Ron Webb
154
+ :since: 1.0.0
155
+ """
156
+ def text(tag: str) -> str:
157
+ child_el = dep_el.find(self._tag(tag, namespace))
158
+ raw = child_el.text.strip() if child_el is not None and child_el.text else ""
159
+ return self._resolve_value(raw, properties)
160
+
161
+ group_id = text("groupId")
162
+ artifact_id = text("artifactId")
163
+ version = text("version")
164
+ scope = text("scope") or "compile"
165
+
166
+ if not group_id or not artifact_id:
167
+ return None
168
+
169
+ if scope not in RUNTIME_SCOPES:
170
+ _logger.debug("Skipping dependency %s:%s (scope=%s)", group_id, artifact_id, scope)
171
+ return None
172
+
173
+ if not version:
174
+ _logger.warning("No version for %s:%s — skipping", group_id, artifact_id)
175
+ return None
176
+
177
+ return Dependency(
178
+ group_id=group_id,
179
+ artifact_id=artifact_id,
180
+ version=version,
181
+ scope=scope,
182
+ )
@@ -0,0 +1,11 @@
1
+ """
2
+ reporters package.
3
+
4
+ Provides output formatters for vulnerability scan results.
5
+
6
+ :author: Ron Webb
7
+ :since: 1.0.0
8
+ """
9
+
10
+ __author__ = "Ron Webb"
11
+ __since__ = "1.0.0"
@@ -0,0 +1,33 @@
1
+ """
2
+ base module.
3
+
4
+ Defines the abstract base class for scan result reporters.
5
+
6
+ :author: Ron Webb
7
+ :since: 1.0.0
8
+ """
9
+
10
+ from abc import ABC, abstractmethod
11
+
12
+ from ..models.report import ScanResult
13
+
14
+ __author__ = "Ron Webb"
15
+ __since__ = "1.0.0"
16
+
17
+
18
+ class Reporter(ABC):
19
+ """
20
+ Abstract base class for all scan result reporters.
21
+
22
+ :author: Ron Webb
23
+ :since: 1.0.0
24
+ """
25
+
26
+ @abstractmethod
27
+ def report(self, result: ScanResult, output_path: str) -> None:
28
+ """
29
+ Write the scan result to the given output path.
30
+
31
+ :author: Ron Webb
32
+ :since: 1.0.0
33
+ """
@@ -0,0 +1,82 @@
1
+ """
2
+ html_reporter module.
3
+
4
+ Renders vulnerability scan results as an HTML report using Jinja2.
5
+
6
+ :author: Ron Webb
7
+ :since: 1.0.0
8
+ """
9
+
10
+ from pathlib import Path
11
+
12
+ from jinja2 import Environment, FileSystemLoader, select_autoescape
13
+
14
+ from ..models.dependency import Dependency
15
+ from ..models.report import ScanResult
16
+ from ..util.logger import setup_logger
17
+ from .base import Reporter
18
+
19
+ __author__ = "Ron Webb"
20
+ __since__ = "1.0.0"
21
+
22
+ _logger = setup_logger(__name__)
23
+ _TEMPLATES_DIR = Path(__file__).parent / "templates"
24
+
25
+
26
+ class HtmlReporter(Reporter):
27
+ """
28
+ Renders a ScanResult to an HTML report file using the Jinja2 template engine.
29
+
30
+ :author: Ron Webb
31
+ :since: 1.0.0
32
+ """
33
+
34
+ def __init__(self) -> None:
35
+ """
36
+ Initialise the Jinja2 environment pointing at the bundled templates directory.
37
+
38
+ :author: Ron Webb
39
+ :since: 1.0.0
40
+ """
41
+ self._env = Environment(
42
+ loader=FileSystemLoader(str(_TEMPLATES_DIR)),
43
+ autoescape=select_autoescape(["html"]),
44
+ )
45
+
46
+ def report(self, result: ScanResult, output_path: str) -> None:
47
+ """
48
+ Render the scan result to an HTML file at the given output path.
49
+
50
+ :author: Ron Webb
51
+ :since: 1.0.0
52
+ """
53
+ template = self._env.get_template("report.html")
54
+ all_deps = self._flatten_dependencies(result.dependencies)
55
+ html = template.render(result=result, all_deps=all_deps)
56
+
57
+ _logger.info("Writing HTML report to %s", output_path)
58
+ with open(output_path, "w", encoding="utf-8") as file_handle:
59
+ file_handle.write(html)
60
+ _logger.info("HTML report written: %s", output_path)
61
+
62
+ def _flatten_dependencies(self, deps: list[Dependency]) -> list[Dependency]:
63
+ """
64
+ Flatten the dependency tree into a single ordered list for tabular display.
65
+
66
+ :author: Ron Webb
67
+ :since: 1.0.0
68
+ """
69
+ result: list[Dependency] = []
70
+ self._collect(deps, result)
71
+ return result
72
+
73
+ def _collect(self, deps: list[Dependency], result: list[Dependency]) -> None:
74
+ """
75
+ Recursively append dependencies to the result list (pre-order traversal).
76
+
77
+ :author: Ron Webb
78
+ :since: 1.0.0
79
+ """
80
+ for dep in deps:
81
+ result.append(dep)
82
+ self._collect(dep.transitive_dependencies, result)
@@ -0,0 +1,52 @@
1
+ """
2
+ json_reporter module.
3
+
4
+ Writes vulnerability scan results to a JSON file.
5
+
6
+ :author: Ron Webb
7
+ :since: 1.0.0
8
+ """
9
+
10
+ import json
11
+ from dataclasses import asdict
12
+
13
+ from ..models.report import ScanResult
14
+ from ..util.logger import setup_logger
15
+ from .base import Reporter
16
+
17
+ __author__ = "Ron Webb"
18
+ __since__ = "1.0.0"
19
+
20
+ _logger = setup_logger(__name__)
21
+
22
+
23
+ class JsonReporter(Reporter):
24
+ """
25
+ Serialises a ScanResult to a formatted JSON file.
26
+
27
+ :author: Ron Webb
28
+ :since: 1.0.0
29
+ """
30
+
31
+ def report(self, result: ScanResult, output_path: str) -> None:
32
+ """
33
+ Write the scan result as pretty-printed JSON to the given path.
34
+
35
+ :author: Ron Webb
36
+ :since: 1.0.0
37
+ """
38
+ data = {
39
+ "source_file": result.source_file,
40
+ "scanned_at": result.scanned_at,
41
+ "summary": {
42
+ "total_dependencies": result.total_dependencies,
43
+ "total_vulnerabilities": result.total_vulnerabilities,
44
+ "vulnerable_dependency_count": len(result.vulnerable_dependencies),
45
+ },
46
+ "dependencies": [asdict(dep) for dep in result.dependencies],
47
+ }
48
+
49
+ _logger.info("Writing JSON report to %s", output_path)
50
+ with open(output_path, "w", encoding="utf-8") as file_handle:
51
+ json.dump(data, file_handle, indent=2, default=str)
52
+ _logger.info("JSON report written: %s", output_path)