Skip to content
Merged
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
@@ -0,0 +1,27 @@
# 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.
type: object
properties:
sequence_number:
type: integer
format: int32
event:
type: object
additionalProperties: true
required:
- sequence_number
Comment thread
nscuro marked this conversation as resolved.
- event
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# 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.
type: object
allOf:
- $ref: "./paginated-response.yaml"
properties:
events:
type: array
items:
$ref: "./list-workflow-run-events-response-item.yaml"
required:
- _pagination
- events
4 changes: 4 additions & 0 deletions api/src/main/openapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ paths:
$ref: "./paths/workflow-instances__id_.yaml"
/workflow-runs:
$ref: "./paths/workflow-runs.yaml"
/workflow-runs/{id}:
$ref: "./paths/workflow-runs__id_.yaml"
/workflow-runs/{id}/events:
$ref: "./paths/workflow-runs__id__events.yaml"

components:
securitySchemes:
Expand Down
52 changes: 52 additions & 0 deletions api/src/main/openapi/paths/workflow-runs__id_.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 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.
get:
operationId: getWorkflowRun
summary: Get a workflow run
description: |-
Returns metadata of a given workflow run.

**Note:** This is an internal API endpoint and may change without notice.

Requires the `SYSTEM_CONFIGURATION` or `SYSTEM_CONFIGURATION_READ` permission.
tags:
- Workflows
parameters:
- name: id
description: ID of the workflow run
in: path
schema:
type: string
format: uuid
required: true
responses:
"200":
description: Workflow run metadata
content:
application/json:
schema:
$ref: "../components/schemas/workflow-run-metadata.yaml"
"400":
$ref: "../components/responses/invalid-request-error.yaml"
"401":
$ref: "../components/responses/generic-unauthorized-error.yaml"
"403":
$ref: "../components/responses/generic-forbidden-error.yaml"
"404":
$ref: "../components/responses/generic-not-found-error.yaml"
default:
$ref: "../components/responses/generic-error.yaml"
63 changes: 63 additions & 0 deletions api/src/main/openapi/paths/workflow-runs__id__events.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# 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.
get:
operationId: listWorkflowRunEvents
summary: List all events of a workflow run
description: |-
Returns a paginated list of workflow run events, sorted by sequence number.

**Note:** This is an internal API endpoint and may change without notice.

Requires the `SYSTEM_CONFIGURATION` or `SYSTEM_CONFIGURATION_READ` permission.
tags:
- Workflows
parameters:
- name: id
description: ID of the workflow run
in: path
schema:
type: string
format: uuid
required: true
- name: from_sequence_number
description: |-
Sequence number of the last seen event.
May be used to continuously poll for new events.
Can not be used together with `page_token`.
in: query
schema:
type: integer
format: int32
minimum: 0
- $ref: "../components/parameters/pagination-limit.yaml"
- $ref: "../components/parameters/page-token.yaml"
- $ref: "../components/parameters/sort-direction.yaml"
responses:
"200":
description: Paginated list of workflow run events
content:
application/json:
schema:
$ref: "../components/schemas/list-workflow-run-events-response.yaml"
"400":
$ref: "../components/responses/invalid-request-error.yaml"
"401":
$ref: "../components/responses/generic-unauthorized-error.yaml"
"403":
$ref: "../components/responses/generic-forbidden-error.yaml"
default:
$ref: "../components/responses/generic-error.yaml"
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@
import alpine.server.filters.HeaderFilter;
import alpine.server.filters.RequestIdFilter;
import alpine.server.filters.RequestMdcEnrichmentFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.ws.rs.ext.ContextResolver;
import org.dependencytrack.cache.CacheManagerBinder;
import org.dependencytrack.dex.DexEngineBinder;
import org.dependencytrack.filters.JerseyMetricsApplicationEventListener;
import org.dependencytrack.plugin.PluginManagerBinder;
import org.dependencytrack.secret.SecretManagerBinder;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.media.multipart.MultiPartFeature;

Expand All @@ -50,6 +53,15 @@ public ResourceConfig() {
property(PROVIDER_SCANNING_RECURSIVE, true);
property(WADL_FEATURE_DISABLE, true);

final var objectMapper = new ObjectMapper();
Comment thread
nscuro marked this conversation as resolved.
register((ContextResolver<ObjectMapper>) type -> objectMapper);
register(new AbstractBinder() {
@Override
protected void configure() {
bind(objectMapper).to(ObjectMapper.class);
}
});

register(ApiFilter.class);
register(AuthenticationFeature.class);
register(AuthorizationFeature.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,34 +19,74 @@
package org.dependencytrack.resources.v2;

import alpine.server.auth.PermissionRequired;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.protobuf.util.JsonFormat;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;
import org.dependencytrack.api.v2.WorkflowsApi;
import org.dependencytrack.api.v2.model.ListWorkflowRunEventsResponse;
import org.dependencytrack.api.v2.model.ListWorkflowRunEventsResponseItem;
import org.dependencytrack.api.v2.model.ListWorkflowRunsResponse;
import org.dependencytrack.api.v2.model.SortDirection;
import org.dependencytrack.api.v2.model.WorkflowRunStatus;
import org.dependencytrack.auth.Permissions;
import org.dependencytrack.common.pagination.Page;
import org.dependencytrack.dex.engine.api.DexEngine;
import org.dependencytrack.dex.engine.api.WorkflowRunHistoryEntry;
import org.dependencytrack.dex.engine.api.WorkflowRunMetadata;
import org.dependencytrack.dex.engine.api.request.ListWorkflowRunHistoryRequest;
import org.dependencytrack.dex.engine.api.request.ListWorkflowRunsRequest;
import org.dependencytrack.dex.proto.event.v1.ActivityTaskCompleted;
import org.dependencytrack.dex.proto.event.v1.ActivityTaskFailed;
import org.dependencytrack.dex.proto.event.v1.ChildRunCompleted;
import org.dependencytrack.dex.proto.event.v1.ChildRunFailed;
import org.dependencytrack.dex.proto.event.v1.TimerElapsed;
import org.dependencytrack.dex.proto.event.v1.WorkflowEvent;
import org.dependencytrack.proto.internal.workflow.v1.ArgumentCommon;
import org.dependencytrack.proto.internal.workflow.v1.ArgumentCsaf;
import org.dependencytrack.proto.internal.workflow.v1.ArgumentNotification;
import org.dependencytrack.resources.AbstractApiResource;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.time.Instant;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

@Path("/")
@NullMarked
public class WorkflowsResource extends AbstractApiResource implements WorkflowsApi {

private final DexEngine dexEngine;
private final ObjectMapper objectMapper;
private final JsonFormat.Printer eventJsonPrinter;

@Inject
WorkflowsResource(DexEngine dexEngine) {
WorkflowsResource(DexEngine dexEngine, ObjectMapper objectMapper) {
this.dexEngine = dexEngine;
this.objectMapper = objectMapper;
this.eventJsonPrinter = JsonFormat.printer()
// Ensure that event IDs with value 0 are not omitted.
.includingDefaultValueFields(Set.of(
WorkflowEvent.getDescriptor().findFieldByName("id"),
ActivityTaskCompleted.getDescriptor().findFieldByName("activity_task_created_event_id"),
ActivityTaskFailed.getDescriptor().findFieldByName("activity_task_created_event_id"),
ChildRunCompleted.getDescriptor().findFieldByName("child_run_created_event_id"),
ChildRunFailed.getDescriptor().findFieldByName("child_run_created_event_id"),
TimerElapsed.getDescriptor().findFieldByName("timer_created_event_id")))
// Register message types that are used in Any fields.
.usingTypeRegistry(
JsonFormat.TypeRegistry.newBuilder()
.add(ArgumentCommon.getDescriptor().getMessageTypes())
.add(ArgumentCsaf.getDescriptor().getMessageTypes())
.add(ArgumentNotification.getDescriptor().getMessageTypes())
.build());
}

@Override
Expand Down Expand Up @@ -105,11 +145,7 @@ public Response listWorkflowRuns(
case "completed_at" -> ListWorkflowRunsRequest.SortBy.COMPLETED_AT;
case null, default -> null;
})
.withSortDirection(switch (sortDirection) {
case ASC -> org.dependencytrack.common.pagination.SortDirection.ASC;
case DESC -> org.dependencytrack.common.pagination.SortDirection.DESC;
case null -> null;
})
.withSortDirection(convert(sortDirection))
.withPageToken(pageToken)
.withLimit(limit));

Expand All @@ -123,6 +159,49 @@ public Response listWorkflowRuns(
return Response.ok(response).build();
}

@Override
@PermissionRequired({
Permissions.Constants.SYSTEM_CONFIGURATION,
Permissions.Constants.SYSTEM_CONFIGURATION_READ
})
public Response getWorkflowRun(UUID id) {
final WorkflowRunMetadata runMetadata = dexEngine.getRunMetadataById(id);
if (runMetadata == null) {
throw new NotFoundException();
}

return Response.ok(convert(runMetadata)).build();
}

@Override
@PermissionRequired({
Permissions.Constants.SYSTEM_CONFIGURATION,
Permissions.Constants.SYSTEM_CONFIGURATION_READ
})
public Response listWorkflowRunEvents(
UUID id,
@Nullable Integer fromSequenceNumber,
Integer limit,
@Nullable String pageToken,
@Nullable SortDirection sortDirection) {
final Page<WorkflowRunHistoryEntry> historyEntryPage =
dexEngine.listRunHistory(
new ListWorkflowRunHistoryRequest(id)
.withFromSequenceNumber(fromSequenceNumber)
.withSortDirection(convert(sortDirection))
.withPageToken(pageToken)
.withLimit(limit));

final var response = ListWorkflowRunEventsResponse.builder()
.events(historyEntryPage.items().stream()
.map(entry -> convert(entry, eventJsonPrinter, objectMapper))
.toList())
.pagination(createPaginationMetadata(getUriInfo(), historyEntryPage))
.build();

return Response.ok(response).build();
}

private static org.dependencytrack.dex.engine.api.@Nullable WorkflowRunStatus convert(@Nullable WorkflowRunStatus status) {
return switch (status) {
case CANCELLED -> org.dependencytrack.dex.engine.api.WorkflowRunStatus.CANCELLED;
Expand Down Expand Up @@ -170,4 +249,32 @@ private static org.dependencytrack.api.v2.model.WorkflowRunMetadata convert(Work
.build();
}

private static ListWorkflowRunEventsResponseItem convert(
WorkflowRunHistoryEntry entry,
JsonFormat.Printer eventJsonPrinter,
ObjectMapper objectMapper) {
final Map<String, Object> eventJsonMap;
try {
final String eventJson = eventJsonPrinter.print(entry.event());
eventJsonMap = objectMapper.readValue(eventJson, new TypeReference<>() {
});
} catch (IOException e) {
throw new UncheckedIOException(e);
}

return ListWorkflowRunEventsResponseItem.builder()
.sequenceNumber(entry.sequenceNumber())
.event(eventJsonMap)
.build();
}

private static org.dependencytrack.common.pagination.@Nullable SortDirection convert(
@Nullable SortDirection sortDirection) {
return switch (sortDirection) {
case ASC -> org.dependencytrack.common.pagination.SortDirection.ASC;
case DESC -> org.dependencytrack.common.pagination.SortDirection.DESC;
case null -> null;
};
}

}
Loading
Loading