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 @@ -41,6 +41,8 @@ properties:
maxLength: 255
internal:
type: boolean
direct_dependency:
type: boolean
copyright:
type: string
maxLength: 255
Expand Down
16 changes: 16 additions & 0 deletions apiserver/src/main/java/org/dependencytrack/model/Component.java
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,11 @@ public enum FetchGroup {
@NotNull
private UUID uuid;

@Persistent
@Column(name = "DIRECT_DEPENDENCY", allowsNull = "true")
@JsonProperty("isDirectDependency")
private Boolean directDependency;

private transient String bomRef;
private transient List<org.cyclonedx.model.License> licenseCandidates;
private transient DependencyMetrics metrics;
Expand Down Expand Up @@ -955,6 +960,17 @@ public void setOccurrenceCount(final Long occurrenceCount) {
this.occurrenceCount = occurrenceCount;
}

public boolean isDirectDependency() {
if (directDependency == null) {
return false;
}
return directDependency;
}

public void setDirectDependency(boolean directDependency) {
this.directDependency = directDependency;
}

@Override
public String toString() {
if (getPurl() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ public enum Subject {
VERSION(PolicyViolation.Type.OPERATIONAL),
COMPONENT_HASH(PolicyViolation.Type.OPERATIONAL),
IS_INTERNAL(PolicyViolation.Type.OPERATIONAL),
IS_DIRECT_DEPENDENCY(PolicyViolation.Type.OPERATIONAL),
CWE(PolicyViolation.Type.SECURITY),
VULNERABILITY_ID(PolicyViolation.Type.SECURITY),
VERSION_DISTANCE(PolicyViolation.Type.OPERATIONAL),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public static org.dependencytrack.proto.policy.v1.Component mapToProto(final Com
maybeSet(component::getPurl, purl -> protoBuilder.setPurl(purl.canonicalize()));
maybeSet(component::getSwidTagId, protoBuilder::setSwidTagId);
maybeSet(component::isInternal, protoBuilder::setIsInternal);
maybeSet(component::isDirectDependency, protoBuilder::setIsDirectDependency);
maybeSet(component::getMd5, protoBuilder::setMd5);
maybeSet(component::getSha1, protoBuilder::setSha1);
maybeSet(component::getSha256, protoBuilder::setSha256);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ public class ComponentProjection {

public Boolean internal;

public Boolean direct;

public Double lastInheritedRiskScore;

public String md5;
Expand Down Expand Up @@ -178,6 +180,9 @@ public static Component mapToComponent(ComponentProjection result) {
if (result.internal != null) {
componentPersistent.setInternal(result.internal);
}
if (result.direct != null) {
componentPersistent.setDirectDependency(result.direct);
}
componentPersistent.setScope(result.scope != null ? Scope.valueOf(result.scope) : null);
componentPersistent.setNotes(result.text);
componentPersistent.setSwidTagId(result.swidTagId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ public PaginatedResult getComponents(final Project project, final boolean includ
"A0"."FILENAME" AS "filename",
"A0"."GROUP" AS "group",
"A0"."INTERNAL" AS "internal",
"A0"."DIRECT_DEPENDENCY" AS "directDependency",
"A0"."LAST_RISKSCORE" AS "lastInheritedRiskScore",
"A0"."LICENSE" AS "componentLicenseName",
"A0"."LICENSE_EXPRESSION" AS "licenseExpression",
Expand Down Expand Up @@ -211,7 +212,7 @@ AND EXISTS (
if (onlyDirect) {
queryString +=
"""
AND "B0"."DIRECT_DEPENDENCIES" @> JSONB_BUILD_ARRAY(JSONB_BUILD_OBJECT('uuid', "A0"."UUID"))
AND "A0"."DIRECT_DEPENDENCY" = TRUE
""";
}
if (orderBy == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,8 @@ default Page<Component> listProjectComponents(ListProjectComponentsQuery query)
}
if (Boolean.TRUE.equals(query.onlyDirect())) {
whereConditions.add(/* language=SQL */ """
EXISTS (
SELECT 1
FROM "PROJECT"
WHERE "PROJECT"."ID" = "C"."PROJECT_ID"
AND "PROJECT"."DIRECT_DEPENDENCIES" @> JSONB_BUILD_ARRAY(JSONB_BUILD_OBJECT('uuid', "C"."UUID"))
)""");
"C"."DIRECT_DEPENDENCY" IS TRUE
""");
Comment thread
sahibamittal marked this conversation as resolved.
}
if (query.searchText() != null && !query.searchText().isBlank()) {
whereConditions.add(/* language=SQL */ """
Expand Down Expand Up @@ -235,6 +231,7 @@ default Page<Component> listProjectComponents(ListProjectComponentsQuery query)
, "C"."PURL"
, "C"."GROUP"
, "C"."INTERNAL"
, "C"."DIRECT_DEPENDENCY"
, "C"."LAST_RISKSCORE"
, "C"."LICENSE" AS "componentLicenseName"
, "C"."LICENSE_EXPRESSION" AS "licenseExpression"
Expand Down Expand Up @@ -629,5 +626,4 @@ public ListedComponent map(final ResultSet rs, final StatementContext ctx) throw
return new ListedComponent(component, publishedAtMicros);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import org.dependencytrack.policy.cel.compat.CoordinatesCelPolicyScriptSourceBuilder;
import org.dependencytrack.policy.cel.compat.CpeCelPolicyScriptSourceBuilder;
import org.dependencytrack.policy.cel.compat.CweCelPolicyScriptSourceBuilder;
import org.dependencytrack.policy.cel.compat.DirectDependencyCelPolicyScriptSourceBuilder;
import org.dependencytrack.policy.cel.compat.EpssCelPolicyScriptSourceBuilder;
import org.dependencytrack.policy.cel.compat.InternalStatusCelPolicyScriptSourceBuilder;
import org.dependencytrack.policy.cel.compat.LicenseCelPolicyScriptSourceBuilder;
Expand Down Expand Up @@ -94,6 +95,7 @@ public final class CelPolicyEngine {
Map.entry(Subject.EPSS, new EpssCelPolicyScriptSourceBuilder()),
Map.entry(Subject.EXPRESSION, PolicyCondition::getValue),
Map.entry(Subject.IS_INTERNAL, new InternalStatusCelPolicyScriptSourceBuilder()),
Map.entry(Subject.IS_DIRECT_DEPENDENCY, new DirectDependencyCelPolicyScriptSourceBuilder()),
Map.entry(Subject.LICENSE, new LicenseCelPolicyScriptSourceBuilder()),
Map.entry(Subject.LICENSE_GROUP, new LicenseGroupCelPolicyScriptSourceBuilder()),
Map.entry(Subject.PACKAGE_URL, new PackageUrlCelPolicyScriptSourceBuilder()),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* This file is part of Dependency-Track.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) OWASP Foundation. All Rights Reserved.
*/
package org.dependencytrack.policy.cel.compat;

import org.dependencytrack.model.PolicyCondition;

public final class DirectDependencyCelPolicyScriptSourceBuilder implements CelPolicyScriptSourceBuilder {

@Override
public String apply(PolicyCondition policyCondition) {
final boolean conditionValue = Boolean.parseBoolean(policyCondition.getValue());

return switch (policyCondition.getOperator()) {
case IS -> "component.is_direct_dependency == " + conditionValue;
case IS_NOT -> "component.is_direct_dependency != " + conditionValue;
default -> null;
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public Component map(final ResultSet rs, final StatementContext ctx) throws SQLE
maybeSet(rs, "purl", ResultSet::getString, builder::setPurl);
maybeSet(rs, "swid_tag_id", ResultSet::getString, builder::setSwidTagId);
maybeSet(rs, "is_internal", ResultSet::getBoolean, builder::setIsInternal);
maybeSet(rs, "is_direct_dependency", ResultSet::getBoolean, builder::setIsDirectDependency);
maybeSet(rs, "md5", ResultSet::getString, builder::setMd5);
maybeSet(rs, "sha1", ResultSet::getString, builder::setSha1);
maybeSet(rs, "sha256", ResultSet::getString, builder::setSha256);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,14 +361,11 @@ AND EXISTS (
public boolean isDirectDependency(Component component) {
return jdbiHandle
.createQuery("""
SELECT EXISTS (
SELECT 1
FROM "COMPONENT" AS c
INNER JOIN "PROJECT" AS p
ON p."ID" = c."PROJECT_ID"
AND p."DIRECT_DEPENDENCIES" @> JSONB_BUILD_ARRAY(JSONB_BUILD_OBJECT('uuid', :uuid))
SELECT COALESCE((
SELECT c."DIRECT_DEPENDENCY"
FROM "COMPONENT" c
WHERE c."UUID" = CAST(:uuid AS UUID)
)
), FALSE)
""")
.bind("uuid", component.getUuid())
.mapTo(Boolean.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public record FieldMapping(String protoFieldName, String sqlExpression) {
new FieldMapping("purl", "c.\"PURL\""),
new FieldMapping("swid_tag_id", "c.\"SWIDTAGID\""),
new FieldMapping("is_internal", "c.\"INTERNAL\""),
new FieldMapping("is_direct_dependency", "c.\"DIRECT_DEPENDENCY\""),
new FieldMapping("md5", "c.\"MD5\""),
new FieldMapping("sha1", "c.\"SHA1\""),
new FieldMapping("sha256", "c.\"SHA_256\""),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ public Response listProjectComponents(
.cpe(componentRow.getCpe())
.group(componentRow.getGroup())
.internal(componentRow.isInternal())
.directDependency(componentRow.isDirectDependency())
.lastInheritedRiskScore(componentRow.getLastInheritedRiskScore())
.license(componentRow.getLicense())
.licenseExpression(componentRow.getLicenseExpression())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -664,17 +664,19 @@ private void processDependencyGraph(
) {
assertPersistent(project, "Project must be persistent");

Collection<String> directDependencyBomRefs = null;
if (project.getBomRef() != null) {
final Collection<String> directDependencyBomRefs = dependencyGraph.get(project.getBomRef());
directDependencyBomRefs = dependencyGraph.get(project.getBomRef());
if (directDependencyBomRefs == null || directDependencyBomRefs.isEmpty()) {
LOGGER.warn("""
The dependency graph has %d entries, but the project (metadata.component node of the BOM) \
is not one of them; Graph will be incomplete because it is not possible to determine its root\
""".formatted(dependencyGraph.size()));
}
final Collection<String> rootDirectDependencyBomRefs = directDependencyBomRefs;
final String directDependenciesJson = resolveDependenciesJson(
List.of(project.getBomRef()),
sourceBomRef -> sourceBomRef.equals(project.getBomRef()) ? directDependencyBomRefs : null,
sourceBomRef -> sourceBomRef.equals(project.getBomRef()) ? rootDirectDependencyBomRefs : null,
identitiesByBomRef);
if (!Objects.equals(directDependenciesJson, project.getDirectDependencies())) {
project.setDirectDependencies(directDependenciesJson);
Expand All @@ -690,13 +692,33 @@ private void processDependencyGraph(

for (final Component component : componentsByIdentity.values()) {
assertPersistent(component, "Component must be persistent");

// Clear old flags so repeated BOM imports don't retain components
// that were direct in a previous dependency graph.
if (component.isDirectDependency()) {
component.setDirectDependency(false);
}
final String mergedDirectDependenciesJson = resolveMergedDirectDependenciesJson(
component, dependencyGraph, identitiesByBomRef, bomRefsByIdentity);
if (!Objects.equals(mergedDirectDependenciesJson, component.getDirectDependencies())) {
component.setDirectDependencies(mergedDirectDependenciesJson);
}
}

if (directDependencyBomRefs != null) {
for (final String directDependencyBomRef : directDependencyBomRefs) {
final ComponentIdentity directDependencyIdentity = identitiesByBomRef.get(directDependencyBomRef);
if (directDependencyIdentity == null) {
continue;
}

final Component directDependencyComponent = componentsByIdentity.get(directDependencyIdentity);
if (directDependencyComponent != null && !directDependencyComponent.isDirectDependency()) {
directDependencyComponent.setDirectDependency(true);
}
}
}

qm.getPersistenceManager().flush();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public void testGetOutdatedComponents() throws Exception {
}

@Test
public void testGetDirectComponents() throws Exception {
public void testIsDirectComponents() throws Exception {

final Project project = prepareProject();
var components = qm.getComponents(project, false, false, true);
Expand Down Expand Up @@ -94,7 +94,6 @@ public void getUngroupedOutdatedDirectComponentsTest() throws Exception {

private Project prepareProject() throws Exception {
final Project project = qm.createProject("Acme Application", null, null, null, null, null, null, false);
final List<String> directDepencencies = new ArrayList<>();
final List<PackageMetadata> metadataList = new ArrayList<>();
final List<PackageArtifactMetadata> artifactMetadataList = new ArrayList<>();
// Generate 1000 dependencies
Expand All @@ -114,7 +113,7 @@ private Project prepareProject() throws Exception {
// direct depencencies
if (i < 100) {
// 100 direct depencencies, 900 transitive depencencies
directDepencencies.add("{\"uuid\":\"" + component.getUuid() + "\"}");
component.setDirectDependency(true);
}
// Recent & Outdated
if ((i >= 25) && (i < 225)) {
Expand Down Expand Up @@ -151,7 +150,6 @@ private Project prepareProject() throws Exception {
new PackageMetadataDao(handle).upsertAll(metadataList);
new PackageArtifactMetadataDao(handle).upsertAll(artifactMetadataList);
});
project.setDirectDependencies("[" + String.join(",", directDepencencies.toArray(new String[0])) + "]");
return project;
}

Expand Down Expand Up @@ -270,7 +268,6 @@ public void testMappingComponentProjectionWithAllFields() throws Exception {
*/
private Project prepareProjectUngroupedComponents() throws Exception {
final Project project = qm.createProject("Ungrouped Application", null, null, null, null, null, null, false);
final List<String> directDepencencies = new ArrayList<>();
final List<PackageMetadata> metadataList = new ArrayList<>();
final List<PackageArtifactMetadata> artifactMetadataList = new ArrayList<>();
// Generate 10 dependencies
Expand All @@ -284,7 +281,7 @@ private Project prepareProjectUngroupedComponents() throws Exception {
// direct depencencies
if (i < 4) {
// 4 direct depencencies, 6 transitive depencencies
directDepencencies.add("{\"uuid\":\"" + component.getUuid() + "\"}");
component.setDirectDependency(true);
}
// Recent & Outdated
if ((i < 7)) {
Expand Down Expand Up @@ -313,7 +310,6 @@ private Project prepareProjectUngroupedComponents() throws Exception {
new PackageMetadataDao(handle).upsertAll(metadataList);
new PackageArtifactMetadataDao(handle).upsertAll(artifactMetadataList);
});
project.setDirectDependencies("[" + String.join(",", directDepencencies.toArray(new String[0])) + "]");
return project;
}
}
Loading
Loading