Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.blackduck.integration.detectable.detectables.uv;


import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
Expand All @@ -9,20 +10,34 @@
public class UVDetectorOptions {
private final Set<String> excludedDependencyGroups;

private final Set<String> onlyDependencyGroups;

private final Set<String> includedWorkspaceMembers;

private final Set<String> excludedWorkspaceMembers;

public UVDetectorOptions(List<String> excludedDependencyGroups, List<String> includedWorkspaceMembers, List<String> excludedWorkspaceMembers) {
public UVDetectorOptions(List<String> excludedDependencyGroups, List<String> onlyDependencyGroups, List<String> includedWorkspaceMembers, List<String> excludedWorkspaceMembers) {
this.excludedDependencyGroups = new HashSet<>(excludedDependencyGroups);
this.onlyDependencyGroups = new HashSet<>(onlyDependencyGroups);
this.includedWorkspaceMembers = new HashSet<>(includedWorkspaceMembers);
this.excludedWorkspaceMembers = new HashSet<>(excludedWorkspaceMembers);
}
Comment thread
bd-spratikbharti marked this conversation as resolved.

/**
* Backward-compatible constructor that defaults to an empty onlyDependencyGroups list.
*/
public UVDetectorOptions(List<String> excludedDependencyGroups, List<String> includedWorkspaceMembers, List<String> excludedWorkspaceMembers) {
this(excludedDependencyGroups, Collections.emptyList(), includedWorkspaceMembers, excludedWorkspaceMembers);
}

public Set<String> getExcludedDependencyGroups() {
return excludedDependencyGroups.stream().map(String::toLowerCase).collect(Collectors.toSet());
}

public Set<String> getOnlyDependencyGroups() {
return onlyDependencyGroups.stream().map(String::toLowerCase).collect(Collectors.toSet());
}

public Set<String> getIncludedWorkspaceMembers() {
return includedWorkspaceMembers.stream().map(String::toLowerCase).collect(Collectors.toSet());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,23 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

public class UVBuildExtractor {

private static final String TREE_COMMAND = "tree";
private static final String NO_DEDUPE_FLAG = "--no-dedupe";
private static final String ALL_EXTRAS_FLAG = "--all-extras";
private static final String ALL_GROUPS_FLAG = "--all-groups";
private static final String NO_GROUP_FLAG = "--no-group";
private static final String ONLY_GROUP_FLAG = "--only-group";

private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final DetectableExecutableRunner executableRunner;
private final File sourceDirectory;
private final UVTreeDependencyGraphTransformer uvTreeDependencyGraphTransformer;


public UVBuildExtractor(DetectableExecutableRunner executableRunner, File sourceDirectory, UVTreeDependencyGraphTransformer uvTreeDependencyGraphTransformer) {
this.executableRunner = executableRunner;
this.sourceDirectory = sourceDirectory;
Expand All @@ -35,18 +43,8 @@ public UVBuildExtractor(DetectableExecutableRunner executableRunner, File source

public Extraction extract(ExecutableTarget uvExe, UVDetectorOptions uvDetectorOptions, UVTomlParser uvTomlParser) throws ExecutableRunnerException {
try {
List<String> arguments = new ArrayList<>();
arguments.add("tree");
arguments.add("--no-dedupe");

if(!uvDetectorOptions.getExcludedDependencyGroups().isEmpty()) {
for(String group : uvDetectorOptions.getExcludedDependencyGroups()) {
arguments.add("--no-group");
arguments.add(group);
}
}

// run uv tree command
List<String> arguments = buildTreeCommandArguments(uvDetectorOptions);

ExecutableOutput executableOutput = executableRunner.executeSuccessfully(ExecutableUtils.createFromTarget(sourceDirectory, uvExe, arguments));
List<String> uvTreeOutput = executableOutput.getStandardOutputAsList();

Expand All @@ -62,4 +60,54 @@ public Extraction extract(ExecutableTarget uvExe, UVDetectorOptions uvDetectorOp
return new Extraction.Builder().exception(e).build();
}
}

private List<String> buildTreeCommandArguments(UVDetectorOptions uvDetectorOptions) {
List<String> arguments = new ArrayList<>();
arguments.add(TREE_COMMAND);
arguments.add(NO_DEDUPE_FLAG);

Set<String> onlyGroups = uvDetectorOptions.getOnlyDependencyGroups();
Set<String> excludedGroups = uvDetectorOptions.getExcludedDependencyGroups();

if (!onlyGroups.isEmpty()) {
addOnlyGroupArguments(arguments, onlyGroups, excludedGroups);
} else {
addDefaultGroupArguments(arguments, excludedGroups);
}
Comment thread
bd-spratikbharti marked this conversation as resolved.

return arguments;
}

private void addOnlyGroupArguments(List<String> arguments, Set<String> onlyGroups, Set<String> excludedGroups) {
Set<String> conflictingGroups = onlyGroups.stream()
.filter(excludedGroups::contains)
.collect(Collectors.toSet());

if (!conflictingGroups.isEmpty()) {
logger.warn(
"Dependency groups {} are present in both 'detect.uv.dependency.groups.only' and 'detect.uv.dependency.groups.excluded'. "
+ "The exclusion setting takes precedence; these groups will be excluded.",
conflictingGroups
);
}

Set<String> effectiveOnlyGroups = onlyGroups.stream()
.filter(group -> !excludedGroups.contains(group))
.collect(Collectors.toSet());

for (String group : effectiveOnlyGroups) {
arguments.add(ONLY_GROUP_FLAG);
arguments.add(group);
}
}

private void addDefaultGroupArguments(List<String> arguments, Set<String> excludedGroups) {
arguments.add(ALL_EXTRAS_FLAG);
arguments.add(ALL_GROUPS_FLAG);

for (String group : excludedGroups) {
arguments.add(NO_GROUP_FLAG);
arguments.add(group);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ protected void setup() throws IOException {
"│ │ │ │ │ ├── aiohttp v3.12.15 (*)\n" +
"│ │ │ │ │ ├── cryptography v44.0.3 (*)");

addExecutableOutput(uvTreeDependencyOutput, new File("uv").getAbsolutePath(), "tree", "--no-dedupe");
addExecutableOutput(uvTreeDependencyOutput, new File("uv").getAbsolutePath(), "tree", "--no-dedupe", "--all-extras", "--all-groups");
}

@NotNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ protected void setup() throws IOException {
" └── typing-extensions v4.9.0");

// Note: with --no-group dev, pytest and mypy are excluded from output
addExecutableOutput(uvTreeDependencyOutput, new File("uv").getAbsolutePath(), "tree", "--no-dedupe", "--no-group", "dev");
addExecutableOutput(uvTreeDependencyOutput, new File("uv").getAbsolutePath(), "tree", "--no-dedupe", "--all-extras", "--all-groups", "--no-group", "dev");
}

@NotNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ void extractBuildsBasicArguments() throws Exception {
List<String> arguments = captor.getValue().getCommandWithArguments();
assertTrue(arguments.contains("tree"));
assertTrue(arguments.contains("--no-dedupe"));
assertTrue(arguments.contains("--all-extras"));
assertTrue(arguments.contains("--all-groups"));
}

// ==================== Excluded Groups Tests ====================
Expand All @@ -103,6 +105,8 @@ void extractAddsNoGroupFlagsForExcludedGroups() throws Exception {
List<String> arguments = captor.getValue().getCommandWithArguments();
assertTrue(arguments.contains("tree"));
assertTrue(arguments.contains("--no-dedupe"));
assertTrue(arguments.contains("--all-extras"));
assertTrue(arguments.contains("--all-groups"));
assertTrue(arguments.contains("--no-group"));

int devIndex = arguments.indexOf("dev");
Expand Down Expand Up @@ -130,6 +134,8 @@ void extractWithEmptyExcludedGroupsAddsNoFlags() throws Exception {

List<String> arguments = captor.getValue().getCommandWithArguments();

assertTrue(arguments.contains("--all-extras"));
assertTrue(arguments.contains("--all-groups"));
long noGroupCount = arguments.stream().filter(arg -> arg.equals("--no-group")).count();
assertEquals(0, noGroupCount);
}
Expand All @@ -151,11 +157,135 @@ void extractWithSingleExcludedGroup() throws Exception {

List<String> arguments = captor.getValue().getCommandWithArguments();

assertTrue(arguments.contains("--all-extras"));
assertTrue(arguments.contains("--all-groups"));
long noGroupCount = arguments.stream().filter(arg -> arg.equals("--no-group")).count();
assertEquals(1, noGroupCount);
assertTrue(arguments.contains("dev"));
}

// ==================== Only Groups Tests ====================

@Test
void extractAddsOnlyGroupFlagsAndSkipsAllExtrasAllGroups() throws Exception {
UVBuildExtractor extractor = new UVBuildExtractor(executableRunner, tempDir, transformer);
UVDetectorOptions options = new UVDetectorOptions(
Collections.emptyList(), // excludedGroups
Arrays.asList("dev", "lint"), // onlyGroups
Collections.emptyList(),
Collections.emptyList()
);

Extraction extraction = extractor.extract(uvExe, options, tomlParser);
assertExtractionSuccess(extraction);

ArgumentCaptor<Executable> captor = ArgumentCaptor.forClass(Executable.class);
verify(executableRunner).executeSuccessfully(captor.capture());

List<String> arguments = captor.getValue().getCommandWithArguments();
assertTrue(arguments.contains("tree"));
assertTrue(arguments.contains("--no-dedupe"));

// --all-extras and --all-groups should NOT be present when onlyGroups is set
assertTrue(!arguments.contains("--all-extras"), "Expected --all-extras to be absent when onlyGroups is set");
assertTrue(!arguments.contains("--all-groups"), "Expected --all-groups to be absent when onlyGroups is set");

// --only-group flags should be present for each group
long onlyGroupCount = arguments.stream().filter(arg -> arg.equals("--only-group")).count();
assertEquals(2, onlyGroupCount);
assertTrue(arguments.contains("dev"));
assertTrue(arguments.contains("lint"));

// Each group should be preceded by --only-group
int devIndex = arguments.indexOf("dev");
int lintIndex = arguments.indexOf("lint");
assertTrue(devIndex > 0);
assertTrue(lintIndex > 0);
assertEquals("--only-group", arguments.get(devIndex - 1));
assertEquals("--only-group", arguments.get(lintIndex - 1));
}

@Test
void extractWithSingleOnlyGroup() throws Exception {
UVBuildExtractor extractor = new UVBuildExtractor(executableRunner, tempDir, transformer);
UVDetectorOptions options = new UVDetectorOptions(
Collections.emptyList(),
Arrays.asList("dev"),
Collections.emptyList(),
Collections.emptyList()
);

Extraction extraction = extractor.extract(uvExe, options, tomlParser);
assertExtractionSuccess(extraction);

ArgumentCaptor<Executable> captor = ArgumentCaptor.forClass(Executable.class);
verify(executableRunner).executeSuccessfully(captor.capture());

List<String> arguments = captor.getValue().getCommandWithArguments();

assertTrue(!arguments.contains("--all-extras"));
assertTrue(!arguments.contains("--all-groups"));
long onlyGroupCount = arguments.stream().filter(arg -> arg.equals("--only-group")).count();
assertEquals(1, onlyGroupCount);
assertTrue(arguments.contains("dev"));
}

// ==================== Conflict Handling Tests ====================

@Test
void extractExclusionTakesPrecedenceOverOnlyGroup() throws Exception {
UVBuildExtractor extractor = new UVBuildExtractor(executableRunner, tempDir, transformer);
// "dev" is in both only and excluded — exclusion should take precedence
UVDetectorOptions options = new UVDetectorOptions(
Arrays.asList("dev"), // excludedGroups
Arrays.asList("dev", "lint"), // onlyGroups
Collections.emptyList(),
Collections.emptyList()
);

Extraction extraction = extractor.extract(uvExe, options, tomlParser);
assertExtractionSuccess(extraction);

ArgumentCaptor<Executable> captor = ArgumentCaptor.forClass(Executable.class);
verify(executableRunner).executeSuccessfully(captor.capture());

List<String> arguments = captor.getValue().getCommandWithArguments();

// --all-extras and --all-groups should NOT be present (onlyGroups path)
assertTrue(!arguments.contains("--all-extras"));
assertTrue(!arguments.contains("--all-groups"));

// "dev" should be excluded — only "lint" should remain as --only-group
long onlyGroupCount = arguments.stream().filter(arg -> arg.equals("--only-group")).count();
assertEquals(1, onlyGroupCount, "Only 'lint' should remain after 'dev' is excluded");
assertTrue(arguments.contains("lint"));
assertTrue(!arguments.contains("dev"), "'dev' should be excluded from --only-group flags");
}

@Test
void extractAllOnlyGroupsExcludedResultsInNoOnlyGroupFlags() throws Exception {
UVBuildExtractor extractor = new UVBuildExtractor(executableRunner, tempDir, transformer);
// All only-groups are also excluded
UVDetectorOptions options = new UVDetectorOptions(
Arrays.asList("dev", "lint"), // excludedGroups
Arrays.asList("dev", "lint"), // onlyGroups
Collections.emptyList(),
Collections.emptyList()
);

Extraction extraction = extractor.extract(uvExe, options, tomlParser);
assertExtractionSuccess(extraction);

ArgumentCaptor<Executable> captor = ArgumentCaptor.forClass(Executable.class);
verify(executableRunner).executeSuccessfully(captor.capture());

List<String> arguments = captor.getValue().getCommandWithArguments();

// No --only-group flags since all were excluded
long onlyGroupCount = arguments.stream().filter(arg -> arg.equals("--only-group")).count();
assertEquals(0, onlyGroupCount, "No --only-group flags should remain when all are excluded");
}


/**
* Asserts that the extraction completed on the success path.
Expand Down
1 change: 1 addition & 0 deletions documentation/src/main/markdown/currentreleasenotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
* CentOS support in Detect Docker Inspector has been deprecated and will be removed in 12.0.0. For more details, please see [Docker Inspector Release Notes](packagemgrs/docker/releasenotes.md).
* imageinspector.service.port.centos has been deprecated and will be removed in 12.0.0.
* Clarified documentation for `--detect.uv.dependency.groups.excluded`. Optional is not a dependency group in uv but a section defining extras, therefor supplying `optional` as a value has no effect and exclusions must reference the extra name directly (e.g., postgres, redis).
* Added `detect.uv.dependency.groups.only` property for the UV CLI detector. To restrict scanning to specific dependency groups while excluding standard dependencies and optional extras, use this property. When set, Detect limits analysis to the explicitly listed dependency groups defined in the project's pyproject.toml. Multiple groups can be specified as a comma-separated list (e.g., `detect.uv.dependency.groups.only='dev,lint'`). This applies exclusively to groups under the `[dependency-groups]` section; extras under `[project.optional-dependencies]` are not included. If both this property and `detect.uv.dependency.groups.excluded` are configured, the exclusion setting takes precedence for any overlapping groups and Detect will log a warning.
### Resolved issues
* (IDETECT-5125) Fixed failure during Python scans when the `requirements.txt` file contains Python extras syntax using square brackets, e.g.: `kopf[dev]>=1.3`
* (IDETECT-5090) Fixed PIP Native Inspector failure to parse `requirements.txt` lines that contain [PEP 508 environment markers](https://peps.python.org/pep-0508/).
Expand Down
6 changes: 4 additions & 2 deletions documentation/src/main/markdown/packagemgrs/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ UV has two detectors:

### UV CLI detector

UV CLI will run if the uv executable is found along with a pyproject.toml file. It will run uv tree commands to find dependencies for the project.
UV CLI will run if the uv executable is found along with a pyproject.toml file. It will run uv tree commands to find dependencies for the project. By default, the UV CLI detector includes main dependencies, all dependency groups, and all optional extras. Use the `detect.uv.dependency.groups.excluded` property to exclude specific dependency groups from the scan.

To restrict scanning to specific dependency groups while excluding standard dependencies and optional extras, use the `detect.uv.dependency.groups.only` property. When this property is set, Detect limits analysis to the explicitly listed dependency groups defined in the project's pyproject.toml. You can specify multiple groups as a comma-separated list (for example: `detect.uv.dependency.groups.only='dev,lint'`). This configuration applies exclusively to groups defined under the `[dependency-groups]` section; dependencies declared as extras under `[project.optional-dependencies]` are not included when this property is enabled. If both `detect.uv.dependency.groups.only` and `detect.uv.dependency.groups.excluded` are configured, the exclusion setting takes precedence for any overlapping groups, and Detect will log a warning to indicate the conflict.

### UV Lock detector

Expand All @@ -144,6 +146,6 @@ UV Lock detector will parse uv.lock, requirements.txt, or both to find project d

### Dependency and Workspace Inclusions/Exclusions

[UV Properties](../properties/detectors/uv.md) supports exclusion of all the dependency groups specified. Since uv has a concept of workspaces, they can be included and excluded using the properties provided.
[UV Properties](../properties/detectors/uv.md) supports exclusion of dependency groups. By default, the UV detector includes main dependencies, all dependency groups, and all optional extras. Use the `detect.uv.dependency.groups.excluded` property to exclude specific dependency groups from the scan. Since uv has a concept of workspaces, they can be included and excluded using the properties provided.
The workspace member provided in the property should be identical to the key name under tool.uv.sources since dependencies are created under the same key name in the tree and uv.lock file.
For excluding dependency groups and workspaces, presence of uv.lock or uv executable is required.
Loading