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
Expand Up @@ -176,6 +176,13 @@ private List<String> runCargoTreeCommand(

// Command 2: Get included packages dependencies
List<String> includedCommand = buildPackageCommand(includedWorkspaces, dependencyTypeFilter, cargoDetectableOptions);

// Add features flags if required
addFeatureFlags(includedCommand, cargoDetectableOptions);

if (!dependencyTypeFilter.shouldIncludeAll()) {
addEdgeExclusions(includedCommand, cargoDetectableOptions);
}
combinedOutput.addAll(runCargoTreeCommand(directory, cargoExe, includedCommand));

return combinedOutput;
Expand All @@ -191,6 +198,9 @@ private List<String> runCargoTreeCommand(
addWorkspaceFlags(command, cargoDetectableOptions);
}

// Add features flags if required
addFeatureFlags(command, cargoDetectableOptions);

if (!dependencyTypeFilter.shouldIncludeAll()) {
addEdgeExclusions(command, cargoDetectableOptions);
}
Expand Down Expand Up @@ -255,6 +265,42 @@ private void addWorkspaceFlags(List<String> command, CargoDetectableOptions opti
}
}

private void addFeatureFlags(List<String> command, CargoDetectableOptions options) {
List<String> features = options.getIncludedFeatures();
boolean isDefaultFeaturesDisabled = options.isDefaultFeaturesDisabled();

// Handle --no-default-features flag (independent of specific features)
if (isDefaultFeaturesDisabled) {
command.add("--no-default-features");
}

// Handle feature specifications
if (features != null && !features.isEmpty()) {
// Check for special keywords (case-insensitive)
boolean includeAllFeatures = features.stream()
.anyMatch(feature -> "ALL".equalsIgnoreCase(feature.trim()));
boolean includeNoFeatures = features.stream()
.anyMatch(feature -> "NONE".equalsIgnoreCase(feature.trim()));

if (includeNoFeatures) {
// NONE keyword: skip all feature processing
return;
} else if (includeAllFeatures) {
command.add("--all-features");
} else {
// Add each feature with its own --features flag
// This handles edge cases like features starting with digits (e.g., "2d", "3d")
for (String feature : features) {
String trimmedFeature = feature.trim();
if (!trimmedFeature.isEmpty()) {
command.add("--features");
command.add(trimmedFeature);
}
}
}
}
}

private void addEdgeExclusions(List<String> cargoTreeCommand, CargoDetectableOptions options) {
Map<CargoDependencyType, String> exclusionMap = new EnumMap<>(CargoDependencyType.class);
exclusionMap.put(CargoDependencyType.NORMAL, "no-normal");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,27 @@
public class CargoDetectableOptions {
private final EnumListFilter<CargoDependencyType> dependencyTypeFilter;
private final boolean cargoIgnoreAllWorkspacesMode;
private final boolean isDefaultFeaturesDisabled;
private final List<String> excludedWorkspaces;
private final List<String> includedWorkspaces;

public CargoDetectableOptions(
EnumListFilter<CargoDependencyType> dependencyTypeFilter,
boolean cargoIgnoreAllWorkspacesMode,
List<String> includedWorkspaces,
List<String> excludedWorkspaces
) {
private final List<String> includedFeatures;

public CargoDetectableOptions(EnumListFilter<CargoDependencyType> dependencyTypeFilter,
boolean cargoIgnoreAllWorkspacesMode,
boolean isDefaultFeaturesDisabled,
List<String> includedWorkspaces,
List<String> excludedWorkspaces,
List<String> includedFeatures) {
this.dependencyTypeFilter = dependencyTypeFilter;
this.cargoIgnoreAllWorkspacesMode = cargoIgnoreAllWorkspacesMode;
this.isDefaultFeaturesDisabled = isDefaultFeaturesDisabled;
this.includedWorkspaces = includedWorkspaces;
this.excludedWorkspaces = excludedWorkspaces;
this.includedFeatures = includedFeatures;
}

public CargoDetectableOptions(EnumListFilter<CargoDependencyType> dependencyTypeFilter) {
this(dependencyTypeFilter, false, new ArrayList<>(), new ArrayList<>());
this(dependencyTypeFilter, false, false, new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
}

public EnumListFilter<CargoDependencyType> getDependencyTypeFilter() {
Expand All @@ -42,4 +46,12 @@ public List<String> getIncludedWorkspaces() {
public List<String> getExcludedWorkspaces() {
return excludedWorkspaces;
}

public List<String> getIncludedFeatures() {
return includedFeatures;
}

public boolean isDefaultFeaturesDisabled() {
return isDefaultFeaturesDisabled;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ public class CargoLockfileExtractor {
private final CargoTomlParser cargoTomlParser;
private final CargoLockPackageDataTransformer cargoLockPackageDataTransformer;
private final CargoLockPackageTransformer cargoLockPackageTransformer;
private static final String FEATURE_FLAGS_NOT_SUPPORTED_WARNING =
"Feature inclusion or exclusion (detect.cargo.included.features, detect.cargo.disable.default.features) " +
"are not supported by the Cargo Lockfile Detector and will be ignored. " +
"Use Cargo CLI Detector for accurate feature-based dependency resolution. " +
"Cargo CLI Detector requires the 'cargo' executable to be available in PATH.";

private static final String PROC_MACRO_EXCLUSION_NOT_SUPPORTED_WARNING =
"PROC_MACRO exclusion is not supported by the Cargo Lockfile Detector and will be ignored. " +
"Supported exclusions for Cargo Lockfile Detector: [NORMAL, BUILD, DEV].";

private static final String VIRTUAL_WORKSPACE_ALL_MEMBERS_EXCLUDED_WARNING =
"Cannot exclude all workspace members for virtual manifest. " +
"Please check your workspace configuration (detect.cargo.ignore.all.workspaces or exclude properties). " +
"Zero components will be reported in SBOM.";

public CargoLockfileExtractor(
CargoTomlParser cargoTomlParser,
Expand All @@ -55,16 +69,14 @@ public CargoLockfileExtractor(
}

public Extraction extract(File cargoLockFile, @Nullable File cargoTomlFile, CargoDetectableOptions cargoDetectableOptions) throws IOException, DetectableException, MissingExternalIdException {
logFeatureFlagWarningIfApplicable(cargoDetectableOptions);
CargoLockData cargoLockData = new Toml().read(cargoLockFile).to(CargoLockData.class);
List<CargoLockPackageData> cargoLockPackageDataList = cargoLockData.getPackages().orElse(new ArrayList<>());
boolean exclusionEnabled = isDependencyExclusionEnabled(cargoDetectableOptions);
boolean isVirtualWorkspace = false;

if (exclusionEnabled && cargoDetectableOptions.getDependencyTypeFilter().shouldExclude(CargoDependencyType.PROC_MACRO)) {
logger.warn(
"PROC_MACRO exclusion is not supported by the Cargo Lockfile Detector and will be ignored. " +
"Supported exclusions for Cargo Lockfile Detector: [NORMAL, BUILD, DEV]. "
);
logger.warn(PROC_MACRO_EXCLUSION_NOT_SUPPORTED_WARNING);
}

if (cargoTomlFile == null && exclusionEnabled) {
Expand All @@ -88,11 +100,7 @@ public Extraction extract(File cargoLockFile, @Nullable File cargoTomlFile, Carg

// Early exit for virtual workspace with all members excluded
if (shouldReturnZeroComponents(cargoDetectableOptions, cargoTomlContents, workspaceRoot, isVirtualWorkspace)) {
logger.warn(
"Cannot exclude all workspace members for virtual manifest. " +
"Please check your workspace configuration (detect.cargo.ignore.all.workspaces or exclude properties). " +
"Zero components will be reported in SBOM."
);
logger.warn(VIRTUAL_WORKSPACE_ALL_MEMBERS_EXCLUDED_WARNING);
return new Extraction.Builder().success(new ArrayList<>()).nameVersionIfPresent(projectNameVersion).build();
}

Expand Down Expand Up @@ -155,6 +163,20 @@ public Extraction extract(File cargoLockFile, @Nullable File cargoTomlFile, Carg
.build();
}

private void logFeatureFlagWarningIfApplicable(CargoDetectableOptions cargoDetectableOptions) {
if (cargoDetectableOptions == null) {
return;
}

boolean hasIncludedFeatures = cargoDetectableOptions.getIncludedFeatures() != null &&
!cargoDetectableOptions.getIncludedFeatures().isEmpty();
boolean hasDisabledDefaultFeatures = cargoDetectableOptions.isDefaultFeaturesDisabled();

if (hasIncludedFeatures || hasDisabledDefaultFeatures) {
logger.warn(FEATURE_FLAGS_NOT_SUPPORTED_WARNING);
}
}

private boolean isDependencyExclusionEnabled(CargoDetectableOptions options) {
if (options == null) {
return false;
Expand Down
10 changes: 9 additions & 1 deletion documentation/src/main/markdown/currentreleasenotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@
* eu.store.scass.blackduck.com - 34.54.213.11
* eu.scass.blackduck.com - 34.54.38.252

## Version 11.2.1
## Version 11.3.0

### New features

* With the addition of new properties `detect.cargo.included.features` and `detect.cargo.disable.default.features` [detect_product_short] now supports cargo features and optional dependencies inclusion or exclusion. Feature support is only available for Cargo CLI Detector; Cargo Lockfile Detector will log a warning if feature properties are provided. See [Cargo](properties/detectors/cargo.md) for details.

### Resolved issues

* (IDETECT-4960) Added support for Cargo features and optional dependencies in Cargo CLI Detector, allowing precise control over which features are included in the SBOM through cargo tree command flags. See [Cargo](properties/detectors/cargo.md) for details.

### Dependency Updates

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,25 @@ private DetectProperties() {
.setExample("crates/workspace-a,crates/workspace-b")
.build();

public static final BooleanProperty DETECT_CARGO_DISABLE_DEFAULT_FEATURES =
BooleanProperty.newBuilder("detect.cargo.disable.default.features", false)
.setInfo("Disable Default Features", DetectPropertyFromVersion.VERSION_11_3_0)
.setHelp("All default features are disabled by the Cargo detector.")
.setGroups(DetectGroup.CARGO, DetectGroup.SOURCE_SCAN)
.build();

public static final CaseSensitiveStringListProperty DETECT_CARGO_INCLUDED_FEATURES =
CaseSensitiveStringListProperty.newBuilder("detect.cargo.included.features")
.setInfo("Cargo Include Features", DetectPropertyFromVersion.VERSION_11_3_0)
.setHelp(
"A comma-separated list of Cargo features (specified by the [feature] manifest in Cargo.toml) to exclude.",
"By default, Detect only includes default features, but will include additional features or all features specified via this property."
)
.setGroups(DetectGroup.CARGO, DetectGroup.SOURCE_SCAN)
.setCategory(DetectCategory.Advanced)
.setExample("feature-a,feature-b")
.build();

public static final NoneEnumListProperty<PipenvDependencyType> DETECT_PIPFILE_DEPENDENCY_TYPES_EXCLUDED =
NoneEnumListProperty.newBuilder("detect.pipfile.dependency.types.excluded", NoneEnum.NONE, PipenvDependencyType.class)
.setInfo("Pipfile Dependency Types Excluded", DetectPropertyFromVersion.VERSION_7_13_0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ public enum DetectPropertyFromVersion implements PropertyVersion {
VERSION_10_5_0("10.5.0"),
VERSION_10_6_0("10.6.0"),
VERSION_11_0_0("11.0.0"),
VERSION_11_2_0("11.2.0");
VERSION_11_2_0("11.2.0"),
VERSION_11_3_0("11.3.0");

private final String version;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,11 @@ public CargoDetectableOptions createCargoDetectableOptions() {
EnumListFilter<CargoDependencyType> dependencyTypeFilter = EnumListFilter.fromExcluded(excludedDependencyTypes);

Boolean cargoIgnoreAllWorkspacesMode = detectConfiguration.getValue(DetectProperties.DETECT_CARGO_IGNORE_ALL_WORKSPACES_MODE);
Boolean isDefaultFeaturesDisabled = detectConfiguration.getValue(DetectProperties.DETECT_CARGO_DISABLE_DEFAULT_FEATURES);
List<String> includedWorkspaces = detectConfiguration.getValue(DetectProperties.DETECT_CARGO_INCLUDED_WORKSPACES);
List<String> excludedWorkspaces = detectConfiguration.getValue(DetectProperties.DETECT_CARGO_EXCLUDED_WORKSPACES);
return new CargoDetectableOptions(dependencyTypeFilter, cargoIgnoreAllWorkspacesMode, includedWorkspaces, excludedWorkspaces);
List<String> includedFeatures = detectConfiguration.getValue(DetectProperties.DETECT_CARGO_INCLUDED_FEATURES);
return new CargoDetectableOptions(dependencyTypeFilter, cargoIgnoreAllWorkspacesMode, isDefaultFeaturesDisabled, includedWorkspaces, excludedWorkspaces, includedFeatures);
}

public PipenvDetectableOptions createPipenvDetectableOptions() {
Expand Down