Skip to content

Commit a2799dd

Browse files
amitmoskovitzclaude
andcommitted
CM-64214: Fix missing dependency paths in Maven CLI scan
Upgrade CycloneDX Maven plugin from 2.7.4 to 2.9.1 for more complete dependency graphs, and fall back to dependency:tree when the generated BOM has no dependency edges. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7749b00 commit a2799dd

3 files changed

Lines changed: 154 additions & 4 deletions

File tree

cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
from os import path
23
from typing import Optional
34

@@ -16,6 +17,17 @@
1617
MAVEN_DEP_TREE_FILE_NAME = 'bcde.mvndeps'
1718

1819

20+
def _has_dependency_graph(bom_file_path: str) -> bool:
21+
try:
22+
content = get_file_content(bom_file_path)
23+
if not content:
24+
return False
25+
bom = json.loads(content)
26+
return any(dep.get('dependsOn') for dep in bom.get('dependencies', []))
27+
except Exception:
28+
return False
29+
30+
1931
class RestoreMavenDependencies(BaseRestoreDependencies):
2032
def __init__(self, ctx: typer.Context, is_git_diff: bool, command_timeout: int) -> None:
2133
super().__init__(ctx, is_git_diff, command_timeout)
@@ -24,7 +36,7 @@ def is_project(self, document: Document) -> bool:
2436
return path.basename(document.path).split('/')[-1] == BUILD_MAVEN_FILE_NAME
2537

2638
def get_commands(self, manifest_file_path: str) -> list[list[str]]:
27-
command = ['mvn', 'org.cyclonedx:cyclonedx-maven-plugin:2.7.4:makeAggregateBom', '-f', manifest_file_path]
39+
command = ['mvn', 'org.cyclonedx:cyclonedx-maven-plugin:2.9.1:makeAggregateBom', '-f', manifest_file_path]
2840

2941
maven_settings_file = self.ctx.obj.get('maven_settings_file')
3042
if maven_settings_file:
@@ -46,10 +58,12 @@ def try_restore_dependencies(self, document: Document) -> Optional[Document]:
4658
if restore_dependencies_document is None:
4759
return None
4860

49-
restore_dependencies_document.content = get_file_content(
50-
join_paths(get_file_dir(manifest_file_path), self.get_lock_file_name())
51-
)
61+
bom_file_path = join_paths(get_file_dir(manifest_file_path), self.get_lock_file_name())
62+
63+
if not _has_dependency_graph(bom_file_path):
64+
return self.restore_from_secondary_command(document, manifest_file_path)
5265

66+
restore_dependencies_document.content = get_file_content(bom_file_path)
5367
return restore_dependencies_document
5468

5569
def restore_from_secondary_command(self, document: Document, manifest_file_path: str) -> Optional[Document]:

tests/cli/files_collector/sca/__init__.py

Whitespace-only changes.
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import json
2+
import os
3+
import tempfile
4+
from unittest.mock import MagicMock, patch
5+
6+
import pytest
7+
8+
from cycode.cli.files_collector.sca.maven.restore_maven_dependencies import (
9+
RestoreMavenDependencies,
10+
_has_dependency_graph,
11+
)
12+
from cycode.cli.models import Document
13+
14+
15+
def _write_bom(tmp_dir: str, bom: dict) -> str:
16+
bom_path = os.path.join(tmp_dir, 'bom.json')
17+
with open(bom_path, 'w') as f:
18+
json.dump(bom, f)
19+
return bom_path
20+
21+
22+
class TestHasDependencyGraph:
23+
def test_returns_false_when_file_does_not_exist(self) -> None:
24+
assert _has_dependency_graph('/nonexistent/path/bom.json') is False
25+
26+
def test_returns_false_when_file_is_empty(self) -> None:
27+
with tempfile.TemporaryDirectory() as tmp_dir:
28+
bom_path = os.path.join(tmp_dir, 'bom.json')
29+
open(bom_path, 'w').close()
30+
assert _has_dependency_graph(bom_path) is False
31+
32+
def test_returns_false_when_dependencies_section_is_missing(self) -> None:
33+
with tempfile.TemporaryDirectory() as tmp_dir:
34+
bom_path = _write_bom(tmp_dir, {'components': [{'name': 'foo'}]})
35+
assert _has_dependency_graph(bom_path) is False
36+
37+
def test_returns_false_when_all_dependencies_have_empty_dependsOn(self) -> None:
38+
with tempfile.TemporaryDirectory() as tmp_dir:
39+
bom = {'dependencies': [{'ref': 'pkg:maven/foo/bar@1.0', 'dependsOn': []}]}
40+
bom_path = _write_bom(tmp_dir, bom)
41+
assert _has_dependency_graph(bom_path) is False
42+
43+
def test_returns_false_when_dependencies_list_is_empty(self) -> None:
44+
with tempfile.TemporaryDirectory() as tmp_dir:
45+
bom_path = _write_bom(tmp_dir, {'dependencies': []})
46+
assert _has_dependency_graph(bom_path) is False
47+
48+
def test_returns_true_when_at_least_one_dependency_has_dependsOn(self) -> None:
49+
with tempfile.TemporaryDirectory() as tmp_dir:
50+
bom = {
51+
'dependencies': [
52+
{'ref': 'pkg:maven/com.example/root@1.0', 'dependsOn': ['pkg:maven/io.netty/netty-all@4.1.0']},
53+
{'ref': 'pkg:maven/io.netty/netty-all@4.1.0', 'dependsOn': []},
54+
]
55+
}
56+
bom_path = _write_bom(tmp_dir, bom)
57+
assert _has_dependency_graph(bom_path) is True
58+
59+
def test_returns_false_when_bom_is_invalid_json(self) -> None:
60+
with tempfile.TemporaryDirectory() as tmp_dir:
61+
bom_path = os.path.join(tmp_dir, 'bom.json')
62+
with open(bom_path, 'w') as f:
63+
f.write('not valid json {{{')
64+
assert _has_dependency_graph(bom_path) is False
65+
66+
67+
class TestRestoreMavenDependenciesFallback:
68+
def _make_instance(self) -> RestoreMavenDependencies:
69+
ctx = MagicMock()
70+
ctx.obj = {}
71+
return RestoreMavenDependencies(ctx=ctx, is_git_diff=False, command_timeout=60)
72+
73+
def test_falls_back_to_secondary_command_when_bom_has_no_dependency_graph(self) -> None:
74+
instance = self._make_instance()
75+
document = MagicMock(spec=Document)
76+
document.content = 'some content'
77+
78+
with tempfile.TemporaryDirectory() as tmp_dir:
79+
pom_path = os.path.join(tmp_dir, 'pom.xml')
80+
open(pom_path, 'w').close()
81+
bom_dir = os.path.join(tmp_dir, 'target')
82+
os.makedirs(bom_dir)
83+
_write_bom(bom_dir, {'dependencies': []})
84+
85+
fallback_doc = MagicMock(spec=Document)
86+
87+
with (
88+
patch.object(instance, 'get_manifest_file_path', return_value=pom_path),
89+
patch(
90+
'cycode.cli.files_collector.sca.maven.restore_maven_dependencies.BaseRestoreDependencies.try_restore_dependencies',
91+
return_value=MagicMock(spec=Document),
92+
),
93+
patch.object(instance, 'restore_from_secondary_command', return_value=fallback_doc) as mock_fallback,
94+
):
95+
result = instance.try_restore_dependencies(document)
96+
97+
mock_fallback.assert_called_once_with(document, pom_path)
98+
assert result is fallback_doc
99+
100+
def test_returns_bom_document_when_dependency_graph_is_present(self) -> None:
101+
instance = self._make_instance()
102+
document = MagicMock(spec=Document)
103+
document.content = 'some content'
104+
105+
with tempfile.TemporaryDirectory() as tmp_dir:
106+
pom_path = os.path.join(tmp_dir, 'pom.xml')
107+
open(pom_path, 'w').close()
108+
bom_dir = os.path.join(tmp_dir, 'target')
109+
os.makedirs(bom_dir)
110+
bom = {
111+
'dependencies': [
112+
{'ref': 'pkg:maven/com.example/root@1.0', 'dependsOn': ['pkg:maven/io.netty/netty@4.1.0']}
113+
]
114+
}
115+
_write_bom(bom_dir, bom)
116+
117+
base_doc = MagicMock(spec=Document)
118+
119+
with (
120+
patch.object(instance, 'get_manifest_file_path', return_value=pom_path),
121+
patch(
122+
'cycode.cli.files_collector.sca.maven.restore_maven_dependencies.BaseRestoreDependencies.try_restore_dependencies',
123+
return_value=base_doc,
124+
),
125+
patch.object(instance, 'restore_from_secondary_command') as mock_fallback,
126+
):
127+
result = instance.try_restore_dependencies(document)
128+
129+
mock_fallback.assert_not_called()
130+
assert result is base_doc
131+
132+
def test_uses_plugin_version_2_9_1(self) -> None:
133+
instance = self._make_instance()
134+
commands = instance.get_commands('/path/to/pom.xml')
135+
assert len(commands) == 1
136+
assert 'org.cyclonedx:cyclonedx-maven-plugin:2.9.1:makeAggregateBom' in commands[0]

0 commit comments

Comments
 (0)