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.
- java_dependency_analyzer/__init__.py +11 -0
- java_dependency_analyzer/cache/__init__.py +11 -0
- java_dependency_analyzer/cache/db.py +101 -0
- java_dependency_analyzer/cache/vulnerability_cache.py +156 -0
- java_dependency_analyzer/cli.py +394 -0
- java_dependency_analyzer/models/__init__.py +11 -0
- java_dependency_analyzer/models/dependency.py +80 -0
- java_dependency_analyzer/models/report.py +108 -0
- java_dependency_analyzer/parsers/__init__.py +11 -0
- java_dependency_analyzer/parsers/base.py +150 -0
- java_dependency_analyzer/parsers/gradle_dep_tree_parser.py +125 -0
- java_dependency_analyzer/parsers/gradle_parser.py +206 -0
- java_dependency_analyzer/parsers/maven_dep_tree_parser.py +123 -0
- java_dependency_analyzer/parsers/maven_parser.py +182 -0
- java_dependency_analyzer/reporters/__init__.py +11 -0
- java_dependency_analyzer/reporters/base.py +33 -0
- java_dependency_analyzer/reporters/html_reporter.py +82 -0
- java_dependency_analyzer/reporters/json_reporter.py +52 -0
- java_dependency_analyzer/reporters/templates/report.html +406 -0
- java_dependency_analyzer/resolvers/__init__.py +11 -0
- java_dependency_analyzer/resolvers/transitive.py +276 -0
- java_dependency_analyzer/scanners/__init__.py +11 -0
- java_dependency_analyzer/scanners/base.py +102 -0
- java_dependency_analyzer/scanners/ghsa_scanner.py +204 -0
- java_dependency_analyzer/scanners/osv_scanner.py +167 -0
- java_dependency_analyzer/util/__init__.py +11 -0
- java_dependency_analyzer/util/logger.py +48 -0
- java_dependency_analyzer-1.0.0.dist-info/METADATA +193 -0
- java_dependency_analyzer-1.0.0.dist-info/RECORD +31 -0
- java_dependency_analyzer-1.0.0.dist-info/WHEEL +4 -0
- 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,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)
|