Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce isLatest project flag & allow policies to be limited to latest version #4184

Merged
12 changes: 12 additions & 0 deletions src/main/java/org/dependencytrack/model/Policy.java
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@
@Column(name = "INCLUDE_CHILDREN", allowsNull = "true") // New column, must allow nulls on existing data bases)
private boolean includeChildren;

@Persistent
@Column(name = "ONLY_LATEST_PROJECT_VERSION", defaultValue = "false")
private boolean onlyLatestProjectVersion = false;

Check warning on line 147 in src/main/java/org/dependencytrack/model/Policy.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/main/java/org/dependencytrack/model/Policy.java#L147

Avoid using redundant field initializer for 'onlyLatestProjectVersion'

public long getId() {
return id;
}
Expand Down Expand Up @@ -224,4 +228,12 @@
public void setIncludeChildren(boolean includeChildren) {
this.includeChildren = includeChildren;
}

public boolean isOnlyLatestProjectVersion() {
return onlyLatestProjectVersion;
}

public void setOnlyLatestProjectVersion(boolean onlyLatestProjectVersion) {
this.onlyLatestProjectVersion = onlyLatestProjectVersion;
}
}
18 changes: 17 additions & 1 deletion src/main/java/org/dependencytrack/model/Project.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonIncludeProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
Expand Down Expand Up @@ -95,7 +96,8 @@
@Persistent(name = "properties"),
@Persistent(name = "tags"),
@Persistent(name = "accessTeams"),
@Persistent(name = "metadata")
@Persistent(name = "metadata"),
@Persistent(name = "isLatest")
}),
@FetchGroup(name = "METADATA", members = {
@Persistent(name = "metadata")
Expand Down Expand Up @@ -270,6 +272,11 @@
@JsonSerialize(nullsUsing = BooleanDefaultTrueSerializer.class)
private Boolean active; // Added in v3.6. Existing records need to be nullable on upgrade.

@Persistent
@Index(name = "PROJECT_IS_LATEST_IDX")
@Column(name = "IS_LATEST", defaultValue = "false")
private boolean isLatest = false; // Added in v4.12.

Check warning on line 278 in src/main/java/org/dependencytrack/model/Project.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/main/java/org/dependencytrack/model/Project.java#L278

Avoid using redundant field initializer for 'isLatest'

@Persistent(table = "PROJECT_ACCESS_TEAMS", defaultFetchGroup = "true")
@Join(column = "PROJECT_ID")
@Element(column = "TEAM_ID")
Expand Down Expand Up @@ -513,6 +520,15 @@
this.active = active;
}

@JsonProperty("isLatest")
public boolean isLatest() {
return isLatest;
}

public void setIsLatest(Boolean latest) {
isLatest = latest != null ? latest : false;
}

public String getBomRef() {
return bomRef;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,13 @@ public Policy getPolicy(final String name) {
* @param violationState the violation state
* @return the created Policy
*/
public Policy createPolicy(String name, Policy.Operator operator, Policy.ViolationState violationState) {
public Policy createPolicy(String name, Policy.Operator operator, Policy.ViolationState violationState,
boolean onlyLatestProjectVersion) {
final Policy policy = new Policy();
policy.setName(name);
policy.setOperator(operator);
policy.setViolationState(violationState);
policy.setOnlyLatestProjectVersion(onlyLatestProjectVersion);
return persist(policy);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ ProjectQueryFilterBuilder withParent(UUID uuid){
return this;
}

public ProjectQueryFilterBuilder onlyLatestVersion() {
filterCriteria.add("(isLatest == true)");
return this;
}

String buildFilter() {
return String.join(" && ", this.filterCriteria);
}
Expand Down
156 changes: 125 additions & 31 deletions src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;

final class ProjectQueryManager extends QueryManager implements IQueryManager {

Expand Down Expand Up @@ -267,6 +268,38 @@ public Project getProject(final String name, final String version) {
return project;
}


/**
* Returns the latest version of a project by its name.
*
* @param name the name of the Project (required)
* @return a Project object representing the latest version, or null if not found
*/
@Override
public Project getLatestProjectVersion(final String name) {
final Query<Project> query = pm.newQuery(Project.class);

final var filterBuilder = new ProjectQueryFilterBuilder()
.withName(name)
.onlyLatestVersion();

final String queryFilter = filterBuilder.buildFilter();
final Map<String, Object> params = filterBuilder.getParams();

preprocessACLs(query, queryFilter, params, false);
query.setFilter(queryFilter);
query.setRange(0, 1);

final Project project = singleResult(query.executeWithMap(params));
if (project != null) {
// set Metrics to prevent extra round trip
project.setMetrics(getMostRecentProjectMetrics(project));
// set ProjectVersions to prevent extra round trip
project.setVersions(getProjectVersions(project));
}
return project;
}

/**
* Returns a list of projects that are accessible by the specified team.
* @param team the team the has access to Projects
Expand Down Expand Up @@ -397,35 +430,35 @@ public PaginatedResult getProjects(final Tag tag) {
* @return the created Project
*/
@Override
public Project createProject(String name, String description, String version, List<Tag> tags, Project parent, PackageURL purl, boolean active, boolean commitIndex) {
public Project createProject(String name, String description, String version, List<Tag> tags, Project parent,
PackageURL purl, boolean active, boolean commitIndex) {
return createProject(name, description, version, tags, parent, purl, active, false, commitIndex);
}

/**
* Creates a new Project.
* @param name the name of the project to create
* @param description a description of the project
* @param version the project version
* @param tags a List of Tags - these will be resolved if necessary
* @param parent an optional parent Project
* @param purl an optional Package URL
* @param active specified if the project is active
* @param commitIndex specifies if the search index should be committed (an expensive operation)
* @return the created Project
*/
@Override
public Project createProject(String name, String description, String version, List<Tag> tags, Project parent,
PackageURL purl, boolean active, boolean isLatest, boolean commitIndex) {
final Project project = new Project();
project.setName(name);
project.setDescription(description);
project.setVersion(version);
if (parent != null ) {
if (!Boolean.TRUE.equals(parent.isActive())){
throw new IllegalArgumentException("An inactive Parent cannot be selected as parent");
}
project.setParent(parent);
}
project.setParent(parent);
project.setPurl(purl);
project.setActive(active);
final Project result = persist(project);

final List<Tag> resolvedTags = resolveTags(tags);
bind(project, resolvedTags);

Event.dispatch(new IndexEvent(IndexEvent.Action.CREATE, result));
Notification.dispatch(new Notification()
.scope(NotificationScope.PORTFOLIO)
.group(NotificationGroup.PROJECT_CREATED)
.title(NotificationConstants.Title.PROJECT_CREATED)
.level(NotificationLevel.INFORMATIONAL)
.content(result.getName() + " was created")
.subject(NotificationUtil.toJson(pm.detachCopy(result)))
);
commitSearchIndex(commitIndex, Project.class);
return result;
project.setIsLatest(isLatest);
return createProject(project, tags, commitIndex);
}

/**
Expand All @@ -443,11 +476,33 @@ public Project createProject(final Project project, List<Tag> tags, boolean comm
if (project.isActive() == null) {
project.setActive(Boolean.TRUE);
}
final Project result = persist(project);
final List<Tag> resolvedTags = resolveTags(tags);
bind(project, resolvedTags);
final Project oldLatestProject = project.isLatest() ? getLatestProjectVersion(project.getName()) : null;
final Project result = callInTransaction(() -> {
// Remove isLatest flag from current latest project version, if the new project will be the latest
if(oldLatestProject != null) {
oldLatestProject.setIsLatest(false);
persist(oldLatestProject);
}

final Project newProject = persist(project);
final List<Tag> resolvedTags = resolveTags(tags);
bind(project, resolvedTags);
return newProject;
});

if(oldLatestProject != null) {
// if we removed isLatest flag from old version, dispatch update event for the old version
Event.dispatch(new IndexEvent(IndexEvent.Action.UPDATE, oldLatestProject));
}
Event.dispatch(new IndexEvent(IndexEvent.Action.CREATE, result));
Notification.dispatch(new Notification()
.scope(NotificationScope.PORTFOLIO)
.group(NotificationGroup.PROJECT_CREATED)
.title(NotificationConstants.Title.PROJECT_CREATED)
.level(NotificationLevel.INFORMATIONAL)
.content(result.getName() + " was created")
.subject(NotificationUtil.toJson(pm.detachCopy(result)))
);
commitSearchIndex(commitIndex, Project.class);
return result;
}
Expand Down Expand Up @@ -480,6 +535,14 @@ public Project updateProject(Project transientProject, boolean commitIndex) {
}
project.setActive(transientProject.isActive());

final Project oldLatestProject;
if(Boolean.TRUE.equals(transientProject.isLatest()) && Boolean.FALSE.equals(project.isLatest())) {
oldLatestProject = getLatestProjectVersion(project.getName());
} else {
oldLatestProject = null;
}
project.setIsLatest(transientProject.isLatest());

if (transientProject.getParent() != null && transientProject.getParent().getUuid() != null) {
if (project.getUuid().equals(transientProject.getParent().getUuid())){
throw new IllegalArgumentException("A project cannot select itself as a parent");
Expand All @@ -497,10 +560,23 @@ public Project updateProject(Project transientProject, boolean commitIndex) {
project.setParent(null);
}

final List<Tag> resolvedTags = resolveTags(transientProject.getTags());
bind(project, resolvedTags);
final Project result = callInTransaction(() -> {
// Remove isLatest flag from current latest project version, if this project will be the latest now
if(oldLatestProject != null) {
oldLatestProject.setIsLatest(false);
persist(oldLatestProject);
}

final List<Tag> resolvedTags = resolveTags(transientProject.getTags());
bind(project, resolvedTags);

final Project result = persist(project);
return persist(project);
});

if(oldLatestProject != null) {
// if we removed isLatest flag from old version, dispatch update event for the old version
Event.dispatch(new IndexEvent(IndexEvent.Action.UPDATE, oldLatestProject));
}
Event.dispatch(new IndexEvent(IndexEvent.Action.UPDATE, result));
commitSearchIndex(commitIndex, Project.class);
return result;
Expand All @@ -516,10 +592,11 @@ public Project clone(
final boolean includeServices,
final boolean includeAuditHistory,
final boolean includeACL,
final boolean includePolicyViolations
final boolean includePolicyViolations,
final boolean makeCloneLatest
) {
final AtomicReference<Project> oldLatestProject = new AtomicReference<>();
final var jsonMapper = new JsonMapper();

final Project clonedProject = callInTransaction(() -> {
final Project source = getObjectByUuid(Project.class, from, Project.FetchGroup.ALL.name());
if (source == null) {
Expand All @@ -533,6 +610,11 @@ public Project clone(
LOGGER.warn("Project was supposed to be cloned to version %s, but that version already exists".formatted(newVersion));
return null;
}
if(makeCloneLatest) {
oldLatestProject.set(source.isLatest() ? source : getLatestProjectVersion(source.getName()));
} else {
oldLatestProject.set(null);
}
Project project = new Project();
project.setAuthors(source.getAuthors());
project.setManufacturer(source.getManufacturer());
Expand All @@ -544,13 +626,21 @@ public Project clone(
project.setVersion(newVersion);
project.setClassifier(source.getClassifier());
project.setActive(source.isActive());
project.setIsLatest(makeCloneLatest);
project.setCpe(source.getCpe());
project.setPurl(source.getPurl());
project.setSwidTagId(source.getSwidTagId());
if (source.getDirectDependencies() != null && includeComponents && includeServices) {
project.setDirectDependencies(source.getDirectDependencies());
}
project.setParent(source.getParent());

// Remove isLatest flag from current latest project version, if this project will be the latest now
if(oldLatestProject.get() != null) {
oldLatestProject.get().setIsLatest(false);
persist(oldLatestProject.get());
}

project = persist(project);

if (source.getMetadata() != null) {
Expand Down Expand Up @@ -703,6 +793,10 @@ public Project clone(
return project;
});

if(oldLatestProject.get() != null) {
// if we removed isLatest flag from old version, dispatch update event for the old version
Event.dispatch(new IndexEvent(IndexEvent.Action.UPDATE, oldLatestProject.get()));
}
Event.dispatch(new IndexEvent(IndexEvent.Action.CREATE, clonedProject));
commitSearchIndex(true, Project.class);
return clonedProject;
Expand Down
17 changes: 14 additions & 3 deletions src/main/java/org/dependencytrack/persistence/QueryManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,10 @@ public Project getProject(final String name, final String version) {
return getProjectQueryManager().getProject(name, version);
}

public Project getLatestProjectVersion(final String name) {
return getProjectQueryManager().getLatestProjectVersion(name);
}

public PaginatedResult getProjects(final Team team, final boolean excludeInactive, final boolean bypass, final boolean onlyRoot) {
return getProjectQueryManager().getProjects(team, excludeInactive, bypass, onlyRoot);
}
Expand Down Expand Up @@ -484,6 +488,10 @@ public List<Tag> resolveTags(final List<Tag> tags) {
public Project createProject(String name, String description, String version, List<Tag> tags, Project parent, PackageURL purl, boolean active, boolean commitIndex) {
return getProjectQueryManager().createProject(name, description, version, tags, parent, purl, active, commitIndex);
}
public Project createProject(String name, String description, String version, List<Tag> tags, Project parent,
PackageURL purl, boolean active, boolean isLatest, boolean commitIndex) {
return getProjectQueryManager().createProject(name, description, version, tags, parent, purl, active, isLatest, commitIndex);
}

public Project createProject(final Project project, List<Tag> tags, boolean commitIndex) {
return getProjectQueryManager().createProject(project, tags, commitIndex);
Expand All @@ -499,9 +507,9 @@ public boolean updateNewProjectACL(Project transientProject, Principal principal

public Project clone(UUID from, String newVersion, boolean includeTags, boolean includeProperties,
boolean includeComponents, boolean includeServices, boolean includeAuditHistory,
boolean includeACL, boolean includePolicyViolations) {
boolean includeACL, boolean includePolicyViolations, boolean makeCloneLatest) {
return getProjectQueryManager().clone(from, newVersion, includeTags, includeProperties,
includeComponents, includeServices, includeAuditHistory, includeACL, includePolicyViolations);
includeComponents, includeServices, includeAuditHistory, includeACL, includePolicyViolations, makeCloneLatest);
}

public Project updateLastBomImport(Project p, Date date, String bomFormat) {
Expand Down Expand Up @@ -675,7 +683,10 @@ public Policy getPolicy(final String name) {
}

public Policy createPolicy(String name, Policy.Operator operator, Policy.ViolationState violationState) {
return getPolicyQueryManager().createPolicy(name, operator, violationState);
return this.createPolicy(name, operator, violationState, false);
}
public Policy createPolicy(String name, Policy.Operator operator, Policy.ViolationState violationState, boolean onlyLatestProjectVersion) {
return getPolicyQueryManager().createPolicy(name, operator, violationState, onlyLatestProjectVersion);
}

public void removeProjectFromPolicies(final Project project) {
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/org/dependencytrack/policy/PolicyEngine.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ private List<PolicyViolation> evaluate(final QueryManager qm, final List<Policy>
final List<PolicyViolation> policyViolations = new ArrayList<>();
final List<PolicyViolation> existingPolicyViolations = qm.detach(qm.getAllPolicyViolations(component));
for (final Policy policy : policies) {
if(policy.isOnlyLatestProjectVersion() && Boolean.FALSE.equals(component.getProject().isLatest())) {
continue;
}
if (policy.isGlobal() || isPolicyAssignedToProject(policy, component.getProject())
|| isPolicyAssignedToProjectTag(policy, component.getProject())) {
LOGGER.debug("Evaluating component (" + component.getUuid() + ") against policy (" + policy.getUuid() + ")");
Expand Down
Loading
Loading