/*
 * 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.resources.v1;

import alpine.persistence.PaginatedResult;
import alpine.server.auth.PermissionRequired;
import alpine.server.resources.AlpineResource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.headers.Header;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityRequirements;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Validator;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.dependencytrack.auth.Permissions;
import org.dependencytrack.model.AffectedVersionAttribution;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.ConfigPropertyConstants;
import org.dependencytrack.model.Cwe;
import org.dependencytrack.model.Project;
import org.dependencytrack.model.Vulnerability;
import org.dependencytrack.model.VulnerableSoftware;
import org.dependencytrack.model.validation.ValidUuid;
import org.dependencytrack.parser.common.resolver.CweResolver;
import org.dependencytrack.persistence.QueryManager;
import org.dependencytrack.resources.v1.openapi.PaginatedApi;
import org.dependencytrack.resources.v1.vo.AffectedComponent;
import org.dependencytrack.resources.v1.vo.AffectedProject;
import org.dependencytrack.tasks.scanners.AnalyzerIdentity;
import org.dependencytrack.util.CvssUtil;
import org.dependencytrack.util.VulnerabilityUtil;
import us.springett.owasp.riskrating.MissingFactorException;
import us.springett.owasp.riskrating.OwaspRiskRating;

import java.math.BigDecimal;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;

/**
 * JAX-RS resources for processing vulnerabilities.
 *
 * @author Steve Springett
 * @since 3.0.0
 */
@Path("/v1/vulnerability")
@Tag(name = "vulnerability")
@SecurityRequirements({
        @SecurityRequirement(name = "ApiKeyAuth"),
        @SecurityRequirement(name = "BearerAuth")
})
public class VulnerabilityResource extends AlpineResource {

    @GET
    @Path("/component/{uuid}")
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(
            summary = "Returns a list of all vulnerabilities for a specific component",
            description = "<p>Requires permission <strong>VIEW_PORTFOLIO</strong></p>"
    )
    @PaginatedApi
    @ApiResponses(value = {
            @ApiResponse(
                    responseCode = "200",
                    description = "A list of all vulnerabilities for a specific component",
                    headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of vulnerabilities", schema = @Schema(format = "integer")),
                    content = @Content(array = @ArraySchema(schema = @Schema(implementation = Vulnerability.class)))
            ),
            @ApiResponse(responseCode = "401", description = "Unauthorized"),
            @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"),
            @ApiResponse(responseCode = "404", description = "The component could not be found")
    })
    @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO)
    public Response getVulnerabilitiesByComponent(@Parameter(description = "The UUID of the component to retrieve vulnerabilities for", schema = @Schema(type = "string", format = "uuid"), required = true)
                                                  @PathParam("uuid") @ValidUuid String uuid,
                                                  @Parameter(description = "Optionally includes suppressed vulnerabilities")
                                                  @QueryParam("suppressed") boolean suppressed) {
        try (QueryManager qm = new QueryManager(getAlpineRequest())) {
            final Component component = qm.getObjectByUuid(Component.class, uuid);
            if (component != null) {
                if (qm.hasAccess(super.getPrincipal(), component.getProject())) {
                    final PaginatedResult result = qm.getVulnerabilities(component, suppressed);
                    return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build();
                } else {
                    return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build();
                }
            } else {
                return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build();
            }
        }
    }

    @GET
    @Path("/project/{uuid}")
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(
            summary = "Returns a list of all vulnerabilities for a specific project",
            description = "<p>Requires permission <strong>VIEW_PORTFOLIO</strong></p>"
    )
    @ApiResponses(value = {
            @ApiResponse(
                    responseCode = "200",
                    description = "A list of all vulnerabilities for a specific project",
                    headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of vulnerabilities", schema = @Schema(format = "integer")),
                    content = @Content(array = @ArraySchema(schema = @Schema(implementation = Vulnerability.class)))
            ),
            @ApiResponse(responseCode = "401", description = "Unauthorized"),
            @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"),
            @ApiResponse(responseCode = "404", description = "The project could not be found")
    })
    @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO)
    public Response getVulnerabilitiesByProject(@Parameter(description = "The UUID of the project to retrieve vulnerabilities for", schema = @Schema(type = "string", format = "uuid"), required = true)
                                                @PathParam("uuid") @ValidUuid String uuid,
                                                @Parameter(description = "Optionally includes suppressed vulnerabilities")
                                                @QueryParam("suppressed") boolean suppressed) {
        try (QueryManager qm = new QueryManager(getAlpineRequest())) {
            final Project project = qm.getObjectByUuid(Project.class, uuid);
            if (project != null) {
                if (qm.hasAccess(super.getPrincipal(), project)) {
                    // TODO: This should honor pagination but it doesn't.
                    final List<Vulnerability> vulnerabilities = qm.getVulnerabilities(project, suppressed);
                    return Response.ok(vulnerabilities).header(TOTAL_COUNT_HEADER, vulnerabilities.size()).build();
                } else {
                    return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build();
                }
            } else {
                return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build();
            }
        }
    }

    @GET
    @Path("/{uuid}")
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(
            summary = "Returns a specific vulnerability",
            description = "<p>Requires permission <strong>VULNERABILITY_MANAGEMENT</strong></p>"
    )
    @ApiResponses(value = {
            @ApiResponse(
                    responseCode = "200",
                    description = "A specific vulnerability",
                    content = @Content(schema = @Schema(implementation = Vulnerability.class))
            ),
            @ApiResponse(responseCode = "401", description = "Unauthorized"),
            @ApiResponse(responseCode = "404", description = "The vulnerability could not be found")
    })
    @PermissionRequired(Permissions.Constants.VULNERABILITY_MANAGEMENT)
    public Response getVulnerabilityByUuid(@Parameter(description = "The UUID of the vulnerability", schema = @Schema(type = "string", format = "uuid"), required = true)
                                           @PathParam("uuid") @ValidUuid String uuid) {
        try (QueryManager qm = new QueryManager()) {
            final Vulnerability vulnerability = qm.getObjectByUuid(Vulnerability.class, uuid);
            if (vulnerability != null) {
                return Response.ok(vulnerability).build();
            } else {
                return Response.status(Response.Status.NOT_FOUND).entity("The vulnerability could not be found.").build();
            }
        }
    }

    @GET
    @Path("/source/{source}/vuln/{vuln}")
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(
            summary = "Returns a specific vulnerability",
            description = "<p>Requires permission <strong>VIEW_PORTFOLIO</strong></p>"
    )
    @ApiResponses(value = {
            @ApiResponse(
                    responseCode = "200",
                    description = "A specific vulnerability",
                    content = @Content(schema = @Schema(implementation = Vulnerability.class))
            ),
            @ApiResponse(responseCode = "401", description = "Unauthorized"),
            @ApiResponse(responseCode = "404", description = "The vulnerability could not be found")
    })
    @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO)
    public Response getVulnerabilityByVulnId(@PathParam("source") String source,
                                             @PathParam("vuln") String vuln) {
        try (QueryManager qm = new QueryManager()) {
            final Vulnerability vulnerability = qm.getVulnerabilityByVulnId(source, vuln);
            if (vulnerability != null) {
                final List<AffectedComponent> affectedComponents = new ArrayList<>();
                for (final VulnerableSoftware vs : vulnerability.getVulnerableSoftware()) {
                    AffectedComponent affectedComponent = new AffectedComponent(vs);
                    final List<AffectedVersionAttribution> attributions = qm.getAffectedVersionAttributions(vulnerability, vs);
                    affectedComponent.setAffectedVersionAttributions(attributions);
                    affectedComponents.add(affectedComponent);
                }
                vulnerability.setAffectedComponents(affectedComponents);
                qm.makeTransient(vulnerability);
                boolean shouldFilter = qm.isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED);
                if (shouldFilter) {
                    Principal principal = super.getPrincipal();
                    vulnerability.setComponents(
                            vulnerability.getComponents().stream().filter(component -> qm.hasAccess(principal,
                                    component.getProject())).toList());
                }
                return Response.ok(vulnerability).build();
            } else {
                return Response.status(Response.Status.NOT_FOUND).entity("The vulnerability could not be found.").build();
            }
        }
    }

    @GET
    @Path("/source/{source}/vuln/{vuln}/projects")
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(
            summary = "Returns a list of all projects affected by a specific vulnerability",
            description = "<p>Requires permission <strong>VIEW_PORTFOLIO</strong></p>"
    )
    @ApiResponses(value = {
            @ApiResponse(
                    responseCode = "200",
                    description = "A list of all projects affected by a specific vulnerability",
                    headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")),
                    content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class)))
            ),
            @ApiResponse(responseCode = "401", description = "Unauthorized"),
            @ApiResponse(responseCode = "404", description = "The vulnerability could not be found")
    })
    @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO)
    public Response getAffectedProject(@PathParam("source") String source,
                                       @PathParam("vuln") String vuln,
                                       @Parameter(description = "Optionally excludes inactive projects from being returned", required = false)
                                       @QueryParam("excludeInactive") boolean excludeInactive) {
        try (QueryManager qm = new QueryManager(getAlpineRequest())) {
            final Vulnerability vulnerability = qm.getVulnerabilityByVulnId(source, vuln);
            if (vulnerability != null) {
                if (excludeInactive) {
                    final List<AffectedProject> filteredProjects = qm.getAffectedProjects(vulnerability).stream().filter(AffectedProject::getActive).toList();
                    final long filteredCount = filteredProjects.size();
                    return Response.ok(filteredProjects).header(TOTAL_COUNT_HEADER, filteredCount).build();
                }
                qm.makeTransient(vulnerability);
                boolean shouldFilter = qm.isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED);
                if (shouldFilter) {
                    Principal principal = super.getPrincipal();
                    vulnerability.setComponents(
                            vulnerability.getComponents().stream().filter(component -> qm.hasAccess(principal,
                                    component.getProject())).toList());
                }

                final List<AffectedProject> projects = qm.getAffectedProjects(vulnerability);
                final long totalCount = projects.size();
                return Response.ok(projects).header(TOTAL_COUNT_HEADER, totalCount).build();
            } else {
                return Response.status(Response.Status.NOT_FOUND).entity("The vulnerability could not be found.").build();
            }
        }
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(
            summary = "Returns a list of all vulnerabilities",
            description = "<p>Requires permission <strong>VIEW_PORTFOLIO</strong></p>"
    )
    @PaginatedApi
    @ApiResponses(value = {
            @ApiResponse(
                    responseCode = "200",
                    description = "A list of all vulnerabilities",
                    headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of vulnerabilities", schema = @Schema(format = "integer")),
                    content = @Content(array = @ArraySchema(schema = @Schema(implementation = Vulnerability.class)))
            ),
            @ApiResponse(responseCode = "401", description = "Unauthorized")
    })
    @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO)
    public Response getAllVulnerabilities() {
        try (QueryManager qm = new QueryManager(getAlpineRequest())) {
            final PaginatedResult result = qm.getVulnerabilities();
            boolean shouldFilter = qm.isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED);
            if (shouldFilter) {
                Principal principal = super.getPrincipal();
                for (final Vulnerability vulnerability : result.getList(Vulnerability.class)) {
                    vulnerability.setComponents(
                            vulnerability.getComponents().stream().filter(component -> qm.hasAccess(principal,
                                    component.getProject())).toList());
                }
            }
            return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build();
        }
    }

    @PUT
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(
            summary = "Creates a new vulnerability",
            description = "<p>Requires permission <strong>VULNERABILITY_MANAGEMENT</strong></p>"
    )
    @ApiResponses(value = {
            @ApiResponse(
                    responseCode = "201",
                    description = "The created vulnerability",
                    content = @Content(schema = @Schema(implementation = Vulnerability.class))
            ),
            @ApiResponse(responseCode = "401", description = "Unauthorized"),
            @ApiResponse(responseCode = "409", description = "A vulnerability with the specified vulnId already exists")
    })
    @PermissionRequired(Permissions.Constants.VULNERABILITY_MANAGEMENT)
    public Response createVulnerability(Vulnerability jsonVulnerability) {
        final Validator validator = super.getValidator();
        failOnValidationError(
                validator.validateProperty(jsonVulnerability, "vulnId"),
                validator.validateProperty(jsonVulnerability, "title"),
                validator.validateProperty(jsonVulnerability, "subTitle"),
                validator.validateProperty(jsonVulnerability, "description"),
                validator.validateProperty(jsonVulnerability, "recommendation"),
                validator.validateProperty(jsonVulnerability, "references"),
                validator.validateProperty(jsonVulnerability, "credits"),
                validator.validateProperty(jsonVulnerability, "created"),
                validator.validateProperty(jsonVulnerability, "published"),
                validator.validateProperty(jsonVulnerability, "updated"),
                validator.validateProperty(jsonVulnerability, "cvssV2Vector"),
                validator.validateProperty(jsonVulnerability, "cvssV3Vector"),
                validator.validateProperty(jsonVulnerability, "owaspRRVector"),
                validator.validateProperty(jsonVulnerability, "vulnerableVersions"),
                validator.validateProperty(jsonVulnerability, "patchedVersions")
        );

        try (QueryManager qm = new QueryManager()) {
            Vulnerability vulnerability = qm.getVulnerabilityByVulnId(
                    Vulnerability.Source.INTERNAL, jsonVulnerability.getVulnId().trim());
            if (vulnerability == null) {
                final List<Integer> cweIds = new ArrayList<>();
                if (jsonVulnerability.getCwes() != null) {
                    for (int i = 0; i < jsonVulnerability.getCwes().size(); i++) {
                        final Cwe cwe = CweResolver.getInstance().lookup(jsonVulnerability.getCwes().get(i));
                        if (cwe != null) {
                            cweIds.add(cwe.getCweId());
                        }
                    }
                    jsonVulnerability.setCwes(cweIds);
                }
                final List<VulnerableSoftware> vsList = new ArrayList<>();
                if (jsonVulnerability.getAffectedComponents() != null) {
                    for (final AffectedComponent ac : jsonVulnerability.getAffectedComponents()) {
                        final VulnerableSoftware vs = ac.toVulnerableSoftware();
                        if (vs != null) {
                            vsList.add(vs);
                        }
                    }
                }
                recalculateScoresAndSeverityFromVectors(jsonVulnerability);
                jsonVulnerability.setSource(Vulnerability.Source.INTERNAL);
                return qm.callInTransaction(() -> {
                    final Vulnerability persistentVuln = qm.createVulnerability(jsonVulnerability, true);
                    qm.synchronizeVulnerableSoftware(persistentVuln, vsList, Vulnerability.Source.INTERNAL);
                    if (persistentVuln.getVulnerableSoftware() != null && !persistentVuln.getVulnerableSoftware().isEmpty()) {
                        persistentVuln.setAffectedComponents(persistentVuln.getVulnerableSoftware().stream()
                                .peek(vs -> vs.setAffectedVersionAttributions(qm.getAffectedVersionAttributions(persistentVuln, vs)))
                                .map(AffectedComponent::new)
                                .toList());
                    }
                    return Response.status(Response.Status.CREATED).entity(persistentVuln).build();
                });
            } else {
                return Response.status(Response.Status.CONFLICT).entity("A vulnerability with the specified vulnId already exists.").build();
            }
        } catch (MissingFactorException | IllegalArgumentException exception) {
            return Response.status(Response.Status.BAD_REQUEST).entity(exception.getMessage()).build();
        }
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(
            summary = "Updates an internal vulnerability",
            description = "<p>Requires permission <strong>VULNERABILITY_MANAGEMENT</strong></p>"
    )
    @ApiResponses(value = {
            @ApiResponse(
                    responseCode = "200",
                    description = "The updated vulnerability",
                    content = @Content(schema = @Schema(implementation = Vulnerability.class))
            ),
            @ApiResponse(responseCode = "401", description = "Unauthorized"),
            @ApiResponse(responseCode = "404", description = "The vulnerability could not be found"),
            @ApiResponse(responseCode = "406", description = "The vulnId may not be changed")
    })
    @PermissionRequired(Permissions.Constants.VULNERABILITY_MANAGEMENT)
    public Response updateVulnerability(Vulnerability jsonVuln) {
        final Validator validator = super.getValidator();
        failOnValidationError(
                validator.validateProperty(jsonVuln, "title"),
                validator.validateProperty(jsonVuln, "subTitle"),
                validator.validateProperty(jsonVuln, "description"),
                validator.validateProperty(jsonVuln, "recommendation"),
                validator.validateProperty(jsonVuln, "references"),
                validator.validateProperty(jsonVuln, "credits"),
                validator.validateProperty(jsonVuln, "created"),
                validator.validateProperty(jsonVuln, "published"),
                validator.validateProperty(jsonVuln, "updated"),
                validator.validateProperty(jsonVuln, "cvssV2Vector"),
                validator.validateProperty(jsonVuln, "cvssV3Vector"),
                validator.validateProperty(jsonVuln, "owaspRRVector"),
                validator.validateProperty(jsonVuln, "vulnerableVersions"),
                validator.validateProperty(jsonVuln, "patchedVersions")
        );
        try (QueryManager qm = new QueryManager()) {
            Vulnerability vulnerability = qm.getObjectByUuid(Vulnerability.class, jsonVuln.getUuid());
            if (vulnerability != null && Vulnerability.Source.INTERNAL.name().equals(vulnerability.getSource())) {
                if (!vulnerability.getVulnId().equals(jsonVuln.getVulnId())) {
                    return Response.status(Response.Status.NOT_ACCEPTABLE).entity("The vulnId may not be changed.").build();
                }

                final List<Integer> cweIds = new ArrayList<>();
                if (jsonVuln.getCwes() != null) {
                    for (int i = 0; i < jsonVuln.getCwes().size(); i++) {
                        final Cwe cwe = CweResolver.getInstance().lookup(jsonVuln.getCwes().get(i));
                        if (cwe != null) {
                            cweIds.add(cwe.getCweId());
                        }
                    }
                    jsonVuln.setCwes(cweIds);
                }

                List<VulnerableSoftware> vsList = new ArrayList<>();
                if (jsonVuln.getAffectedComponents() != null) {
                    for (final AffectedComponent ac : jsonVuln.getAffectedComponents()) {
                        final VulnerableSoftware vs = ac.toVulnerableSoftware();
                        if (vs != null) {
                            vsList.add(vs);
                        }
                    }
                }
                recalculateScoresAndSeverityFromVectors(jsonVuln);
                return qm.callInTransaction(() -> {
                    final Vulnerability persistentVuln = qm.updateVulnerability(jsonVuln, true);
                    qm.synchronizeVulnerableSoftware(persistentVuln, vsList, Vulnerability.Source.INTERNAL);
                    if (persistentVuln.getVulnerableSoftware() != null && !persistentVuln.getVulnerableSoftware().isEmpty()) {
                        persistentVuln.setAffectedComponents(persistentVuln.getVulnerableSoftware().stream()
                                .peek(vs -> vs.setAffectedVersionAttributions(qm.getAffectedVersionAttributions(persistentVuln, vs)))
                                .map(AffectedComponent::new)
                                .toList());
                    }
                    return Response.ok(persistentVuln).build();
                });
            } else {
                return Response.status(Response.Status.NOT_FOUND).entity("The vulnerability could not be found.").build();
            }
        } catch (MissingFactorException missingFactorException) {
            return Response.status(Response.Status.BAD_REQUEST).entity(missingFactorException.getMessage()).build();
        }
    }

    @DELETE
    @Path("/{uuid}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(
            summary = "Deletes a vulnerability",
            description = "<p>Requires permission <strong>VULNERABILITY_MANAGEMENT</strong></p>"
    )
    @ApiResponses(value = {
            @ApiResponse(responseCode = "204", description = "Vulnerability removed successfully"),
            @ApiResponse(responseCode = "401", description = "Unauthorized"),
            @ApiResponse(responseCode = "403", description = "Access to the specified vulnerability is forbidden"),
            @ApiResponse(responseCode = "404", description = "The UUID of the vulnerability could not be found"),
            @ApiResponse(responseCode = "412", description = "Portfolio components or services are affected by this vulnerability. Unable to delete.")
    })
    @PermissionRequired(Permissions.Constants.VULNERABILITY_MANAGEMENT)
    public Response deleteVulnerability(
            @Parameter(description = "The UUID of the vulnerability to delete", schema = @Schema(type = "string", format = "uuid"), required = true)
            @PathParam("uuid") @ValidUuid String uuid) {
        try (QueryManager qm = new QueryManager()) {
            final Vulnerability vulnerability = qm.getObjectByUuid(Vulnerability.class, uuid);
            if (vulnerability != null) {
                if (Vulnerability.Source.INTERNAL.name().equals(vulnerability.getSource())) {
                    if (vulnerability.getComponents().size() > 0) {
                        return Response.status(Response.Status.PRECONDITION_FAILED).entity("Portfolio components or services are affected by this vulnerability. Unable to delete.").build();
                    } else {
                        qm.deleteAffectedVersionAttributions(vulnerability);
                        qm.delete(vulnerability);
                        return Response.status(Response.Status.NO_CONTENT).build();
                    }
                } else {
                    return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified vulnerability is forbidden.").build();
                }
            } else {
                return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the component could not be found.").build();
            }
        }
    }

    @GET
    @Path("/vulnId")
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(
            summary = "Generates an internal vulnerability identifier",
            description = "<p>Requires permission <strong>PORTFOLIO_MANAGEMENT</strong></p>"
    )
    @ApiResponses(value = {
            @ApiResponse(
                    responseCode = "200",
                    description = "An internal vulnerability identifier",
                    content = @Content(schema = @Schema(type = "string"))
            ),
            @ApiResponse(responseCode = "401", description = "Unauthorized")
    })
    @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT)
    public Response generateInternalVulnerabilityIdentifier() {
        final String vulnId = VulnerabilityUtil.randomInternalId();
        return Response.ok(vulnId).build();
    }

    public void recalculateScoresAndSeverityFromVectors(Vulnerability vuln) throws MissingFactorException {
        // Recalculate V2 score based on vector passed to resource and normalize vector
        final var v2 = CvssUtil.parse(vuln.getNormalizedCvssV2Vector());
        if (v2 != null) {
            vuln.applyV2Score(v2);
        }

        // Recalculate V3 score based on vector passed to resource and normalize vector
        final var v3 = CvssUtil.parse(vuln.getCvssV3Vector());
        if (v3 != null) {
            vuln.applyV3Score(v3);
        }

        // Recalculate OWASP RR score based on vector passed to resource
        if (vuln.getOwaspRRVector() != null) {
            final OwaspRiskRating rr = OwaspRiskRating.fromVector(vuln.getOwaspRRVector());
            final us.springett.owasp.riskrating.Score score = rr.calculateScore();
            vuln.setOwaspRRLikelihoodScore(BigDecimal.valueOf(score.getLikelihoodScore()));
            vuln.setOwaspRRTechnicalImpactScore(BigDecimal.valueOf(score.getTechnicalImpactScore()));
            vuln.setOwaspRRBusinessImpactScore(BigDecimal.valueOf(score.getBusinessImpactScore()));
        }

        vuln.setSeverity(VulnerabilityUtil.getSeverity(
                vuln.getSeverity(),
                vuln.getCvssV2BaseScore(),
                vuln.getCvssV3BaseScore(),
                vuln.getOwaspRRLikelihoodScore(),
                vuln.getOwaspRRTechnicalImpactScore(),
                vuln.getOwaspRRBusinessImpactScore()
        ));
    }

    @POST
    @Path("/source/{source}/vuln/{vulnId}/component/{component}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(
            summary = "Assigns a vulnerability to a component",
            description = "<p>Requires permission <strong>PORTFOLIO_MANAGEMENT</strong></p>"
    )
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "Assignment successful"),
            @ApiResponse(responseCode = "401", description = "Unauthorized"),
            @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"),
            @ApiResponse(responseCode = "404", description = "The vulnerability or component could not be found")
    })
    @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT)
    public Response assignVulnerability(@Parameter(description = "The vulnerability source", required = true)
                                        @PathParam("source") String source,
                                        @Parameter(description = "The vulnId", required = true)
                                        @PathParam("vulnId") String vulnId,
                                        @Parameter(description = "The UUID of the component", schema = @Schema(type = "string", format = "uuid"), required = true)
                                        @PathParam("component") @ValidUuid String componentUuid) {
        try (QueryManager qm = new QueryManager()) {
            Vulnerability vulnerability = qm.getVulnerabilityByVulnId(source, vulnId);
            if (vulnerability == null) {
                return Response.status(Response.Status.NOT_FOUND).entity("The vulnerability could not be found.").build();
            }
            final Component component = qm.getObjectByUuid(Component.class, componentUuid);
            if (component != null) {
                if (qm.hasAccess(super.getPrincipal(), component.getProject())) {
                    qm.addVulnerability(vulnerability, component, AnalyzerIdentity.NONE);
                    return Response.ok().build();
                } else {
                    return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build();
                }
            } else {
                return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build();
            }
        }
    }

    @POST
    @Path("/{uuid}/component/{component}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(
            summary = "Assigns a vulnerability to a component",
            description = "<p>Requires permission <strong>PORTFOLIO_MANAGEMENT</strong></p>"
    )
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "Assignment successful"),
            @ApiResponse(responseCode = "401", description = "Unauthorized"),
            @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"),
            @ApiResponse(responseCode = "404", description = "The vulnerability or component could not be found")
    })
    @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT)
    public Response assignVulnerability(@Parameter(description = "The UUID of the vulnerability", schema = @Schema(type = "string", format = "uuid"), required = true)
                                        @PathParam("uuid") @ValidUuid String uuid,
                                        @Parameter(description = "The UUID of the component", schema = @Schema(type = "string", format = "uuid"), required = true)
                                        @PathParam("component") @ValidUuid String componentUuid) {
        try (QueryManager qm = new QueryManager()) {
            Vulnerability vulnerability = qm.getObjectByUuid(Vulnerability.class, uuid);
            if (vulnerability == null) {
                return Response.status(Response.Status.NOT_FOUND).entity("The vulnerability could not be found.").build();
            }
            final Component component = qm.getObjectByUuid(Component.class, componentUuid);
            if (component != null) {
                if (qm.hasAccess(super.getPrincipal(), component.getProject())) {
                    qm.addVulnerability(vulnerability, component, AnalyzerIdentity.NONE);
                    return Response.ok().build();
                } else {
                    return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build();
                }
            } else {
                return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build();
            }
        }
    }

    @DELETE
    @Path("/source/{source}/vuln/{vulnId}/component/{component}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(
            summary = "Removes assignment of a vulnerability from a component",
            description = "<p>Requires permission <strong>PORTFOLIO_MANAGEMENT</strong></p>"
    )
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "Assignment removal successful"),
            @ApiResponse(responseCode = "401", description = "Unauthorized"),
            @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"),
            @ApiResponse(responseCode = "404", description = "The vulnerability or component could not be found")
    })
    @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT)
    public Response unassignVulnerability(@Parameter(description = "The vulnerability source", required = true)
                                          @PathParam("source") String source,
                                          @Parameter(description = "The vulnId", required = true)
                                          @PathParam("vulnId") String vulnId,
                                          @Parameter(description = "The UUID of the component", schema = @Schema(type = "string", format = "uuid"), required = true)
                                          @PathParam("component") @ValidUuid String componentUuid) {
        try (QueryManager qm = new QueryManager()) {
            Vulnerability vulnerability = qm.getVulnerabilityByVulnId(source, vulnId);
            if (vulnerability == null) {
                return Response.status(Response.Status.NOT_FOUND).entity("The vulnerability could not be found.").build();
            }
            final Component component = qm.getObjectByUuid(Component.class, componentUuid);
            if (component != null) {
                if (qm.hasAccess(super.getPrincipal(), component.getProject())) {
                    qm.removeVulnerability(vulnerability, component);
                    return Response.ok().build();
                } else {
                    return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build();
                }
            } else {
                return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build();
            }
        }
    }

    @DELETE
    @Path("/{uuid}/component/{component}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(
            summary = "Removes assignment of a vulnerability from a component",
            description = "<p>Requires permission <strong>PORTFOLIO_MANAGEMENT</strong></p>"
    )
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "Assignment removal successful"),
            @ApiResponse(responseCode = "401", description = "Unauthorized"),
            @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"),
            @ApiResponse(responseCode = "404", description = "The vulnerability or component could not be found")
    })
    @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT)
    public Response unassignVulnerability(@Parameter(description = "The UUID of the vulnerability", schema = @Schema(type = "string", format = "uuid"), required = true)
                                          @PathParam("uuid") @ValidUuid String uuid,
                                          @Parameter(description = "The UUID of the component", schema = @Schema(type = "string", format = "uuid"), required = true)
                                          @PathParam("component") @ValidUuid String componentUuid) {
        try (QueryManager qm = new QueryManager()) {
            Vulnerability vulnerability = qm.getObjectByUuid(Vulnerability.class, uuid);
            if (vulnerability == null) {
                return Response.status(Response.Status.NOT_FOUND).entity("The vulnerability could not be found.").build();
            }
            Component component = qm.getObjectByUuid(Component.class, componentUuid);
            if (component != null) {
                if (qm.hasAccess(super.getPrincipal(), component.getProject())) {
                    qm.removeVulnerability(vulnerability, component);
                    return Response.ok().build();
                } else {
                    return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build();
                }
            } else {
                return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build();
            }
        }
    }
}
