Skip to content

Commit

Permalink
Use COSV on saving a new vulnerability (#2743)
Browse files Browse the repository at this point in the history
- added a new endpoint /save to save cosv manually
- refactored FE
- removed projects from creating
  • Loading branch information
nulls authored Oct 18, 2023
1 parent 1ef7f67 commit 91114b2
Show file tree
Hide file tree
Showing 24 changed files with 225 additions and 288 deletions.
12 changes: 12 additions & 0 deletions db/v-2/tables/cosv-generated-id.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">

<changeSet id="cosv-generated-id-1" author="nulls">
<renameTable oldTableName="vulnerability_generated_id" newTableName="cosv_generated_id"/>
</changeSet>

</databaseChangeLog>
1 change: 1 addition & 0 deletions db/v-2/tables/db.changelog-tables.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
<include file="cosv-file.xml" relativeToChangelogFile="true"/>
<include file="link-between-cosv-file-and-vulnerability-metadata.xml" relativeToChangelogFile="true"/>
<include file="vulnerability-generated-id.xml" relativeToChangelogFile="true"/>
<include file="cosv-generated-id.xml" relativeToChangelogFile="true"/>

<changeSet id="02-tables" author="frolov">
<tagDatabase tag="v2.0-tables"/>
Expand Down
2 changes: 1 addition & 1 deletion save-backend/backend-api-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -10700,7 +10700,7 @@
},
{
"in": "query",
"name": "ids",
"name": "namesToSkip",
"required": false,
"schema": {
"type": "string",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,15 @@ class UsersDetailsController(
fun findByPrefix(
@RequestParam prefix: String,
@RequestParam(required = false, defaultValue = "5") pageSize: Int,
@RequestParam(required = false, defaultValue = "") ids: String,
): Flux<UserInfo> = ids.toMono()
.map { stringIds -> stringIds.split(DATABASE_DELIMITER) }
.map { stringIdList -> stringIdList.filter { it.isNotBlank() }.map { it.toLong() }.toSet() }
.flatMapMany { idList ->
// `userRepository.findByNameStartingWithAndIdNotIn` with empty `idList` results with empty list for some reason
@RequestParam(required = false, defaultValue = "") namesToSkip: String,
): Flux<UserInfo> = namesToSkip.toMono()
.map { stringNames -> stringNames.split(DATABASE_DELIMITER) }
.map { stringNameList -> stringNameList.filter { it.isNotBlank() }.toSet() }
.flatMapMany { nameList ->
// `userRepository.findByNameStartingWithAndIdNotIn` with empty `nameList` results with empty list for some reason
blockingToFlux {
if (idList.isNotEmpty()) {
userRepository.findByNameStartingWithAndIdNotIn(prefix, idList, Pageable.ofSize(pageSize))
if (nameList.isNotEmpty()) {
userRepository.findByNameStartingWithAndNameNotIn(prefix, nameList, Pageable.ofSize(pageSize))
} else {
userRepository.findByNameStartingWith(prefix, Pageable.ofSize(pageSize))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,19 +121,6 @@ class VulnerabilityController(
): Mono<VulnerabilityExt> = vulnerabilityService.getVulnerabilityWithDescriptionByIdentifier(identifier)
.switchIfEmptyToNotFound { "Could not find vulnerability with identifier $identifier" }

@GetMapping("/by-user")
@Operation(
method = "GET",
summary = "Get list of vulnerabilities by user id.",
description = "Get list of vulnerabilities by user id.",
)
@ApiResponse(responseCode = "200", description = "Successfully fetched list of vulnerabilities by user id")
fun getVulnerabilityByUser(
@RequestParam userName: String,
): Flux<VulnerabilityDto> = blockingToFlux {
vulnerabilityService.findByUserNameAndStatus(userName)
}

@GetMapping("/count-by-user")
@Operation(
method = "GET",
Expand Down Expand Up @@ -184,32 +171,6 @@ class VulnerabilityController(
.body(cosvService.getVulnerabilityAsCosvStream(it))
}

@PostMapping("/save")
@Operation(
method = "POST",
summary = "Save vulnerability.",
description = "Save vulnerability.",
)
@ApiResponse(responseCode = "200", description = "Successfully saved vulnerability")
@PreAuthorize("permitAll()")
fun save(
@RequestBody vulnerabilityDto: VulnerabilityDto,
@RequestParam(required = false, defaultValue = "false") isGenerateIdentifier: Boolean,
authentication: Authentication,
): Mono<StringResponse> = vulnerabilityDto.toMono()
.filter { isGenerateIdentifier && vulnerabilityDto.identifier.isEmpty() || vulnerabilityDto.identifier.isNotEmpty() }
.switchIfEmptyToResponseException(HttpStatus.CONFLICT) {
"Identifier is not provided: either set identifier auto-generation and provide no identifier or provide an identifier."
}
.filter { it.validateIdentifier() }
.switchIfEmptyToResponseException(HttpStatus.CONFLICT) {
"Vulnerability Identifier should either be empty or start with one of prefixes: ${VulnerabilityDto.vulnerabilityPrefixes}"
}
.flatMap {
vulnerabilityService.save(vulnerabilityDto, authentication)
}
.map { ResponseEntity.ok("Vulnerability was successfully saved") }

@PostMapping("/update")
@Operation(
method = "POST",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ interface UserRepository : BaseEntityRepository<User>, ValidateRepository {
*/
fun findByNameStartingWithAndIdNotIn(prefix: String, ids: Set<Long>, page: Pageable): Page<User>

/**
* @param prefix
* @param names
* @param page
* @return [Page] of users with names that start with [prefix] and name not in [names]
*/
fun findByNameStartingWithAndNameNotIn(prefix: String, names: Set<String>, page: Pageable): Page<User>

/**
* @param prefix
* @return list of users with names that start with [prefix]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.saveourtool.save.backend.service.vulnerability

import com.saveourtool.save.authservice.utils.userId
import com.saveourtool.save.backend.repository.TagRepository
import com.saveourtool.save.backend.repository.UserRepository
import com.saveourtool.save.backend.service.TagService
import com.saveourtool.save.backend.utils.hasRole
Expand Down Expand Up @@ -46,9 +45,7 @@ class VulnerabilityService(
private val cosvRepository: CosvRepository,
private val vulnerabilityMetadataRepository: VulnerabilityMetadataRepository,
private val lnkVulnerabilityMetadataTagRepository: LnkVulnerabilityMetadataTagRepository,
private val tagRepository: TagRepository,
private val tagService: TagService,
private val vulnerabilityGeneratedIdRepository: VulnerabilityGeneratedIdRepository,
private val vulnerabilityRatingService: VulnerabilityRatingService,
) {
private fun List<VulnerabilityMetadata>.toTagMap() = lnkVulnerabilityMetadataTagRepository.findByVulnerabilityMetadataIdIn(this.map { it.requiredId() })
Expand All @@ -62,17 +59,6 @@ class VulnerabilityService(
*/
fun findByName(name: String) = vulnerabilityMetadataRepository.findByIdentifier(name)?.toDto()?.toVulnerabilityDto()

/**
* @param userName creator of vulnerability
* @return list of vulnerabilities
*/
fun findByUserNameAndStatus(userName: String): List<VulnerabilityDto> {
val user = userRepository.findByName(userName).orNotFound {
"Not found user by name = $userName"
}
return vulnerabilityMetadataRepository.findByUserId(user.requiredId()).map { it.toDto().toVulnerabilityDto() }
}

/**
* @param userName
* @param status
Expand Down Expand Up @@ -200,60 +186,6 @@ class VulnerabilityService(
fun getVulnerabilityWithDescriptionByIdentifier(name: String): Mono<VulnerabilityExt> =
cosvService.getVulnerabilityExt(name)

/**
* @param vulnerabilityDto dto of new vulnerability
* @param authentication auth info of a current user
* @return saved [VulnerabilityMetadataDto]
*/
@Suppress("TOO_LONG_FUNCTION")
@Transactional
fun save(
vulnerabilityDto: VulnerabilityDto,
authentication: Authentication,
): Mono<VulnerabilityMetadataDto> = blockingToMono {
vulnerabilityDto.identifier.ifEmpty {
vulnerabilityGeneratedIdRepository.saveAndFlush(VulnerabilityGeneratedId()).getIdentifier()
}
}
.flatMap { identifier ->
cosvService.generateAndSave(
vulnerabilityDto.copy(
identifier = identifier,
userInfo = UserInfo(authentication.name),
)
)
}
.blockingMap { metadataDto ->
val metadata = vulnerabilityMetadataRepository.findByIdentifier(metadataDto.identifier).orNotFound()

val existedTags = tagRepository.findByNameIn(vulnerabilityDto.tags)
val existedTagNames = existedTags.map { tag -> tag.name }
val notFoundTagNames = vulnerabilityDto.tags.filter { it !in existedTagNames }
val newTags = tagRepository.saveAll(notFoundTagNames.map { Tag(it) })

vulnerabilityMetadataProjectRepository.saveAll(
vulnerabilityDto.projects.map { dto -> dto.toEntity(metadata) }
)

lnkVulnerabilityMetadataUserRepository.saveAll(
vulnerabilityDto.participants.map { userDto ->

val participant = userRepository.findByName(userDto.name)
.orNotFound { "Not found user by name ${userDto.name}" }

LnkVulnerabilityMetadataUser(
vulnerabilityMetadataId = metadata.requiredId(),
user = participant,
)
}
)

val tagLinks = existedTags.plus(newTags).map { LnkVulnerabilityMetadataTag(metadata, it) }
lnkVulnerabilityMetadataTagRepository.saveAll(tagLinks)

metadataDto
}

/**
* @param vulnerabilityExt
* @param authentication
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import org.springframework.context.annotation.Import
MockBean(RawCosvFileRepository::class),
MockBean(CosvFileRepository::class),
MockBean(BlockingBridge::class),
MockBean(VulnerabilityGeneratedIdRepository::class),
MockBean(CosvGeneratedIdRepository::class),
)
class DatabaseTest {
@Autowired
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ import kotlin.io.path.*
MockBean(RawCosvFileRepository::class),
MockBean(CosvFileRepository::class),
MockBean(BlockingBridge::class),
MockBean(VulnerabilityGeneratedIdRepository::class),
MockBean(CosvGeneratedIdRepository::class),
)
class DownloadFilesTest {
private val organization = Organization.stub(2).apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import org.springframework.context.annotation.Import
MockBean(RawCosvFileRepository::class),
MockBean(CosvFileRepository::class),
MockBean(BlockingBridge::class),
MockBean(VulnerabilityGeneratedIdRepository::class),
MockBean(CosvGeneratedIdRepository::class),
)
class JpaSpecificationTest {
@Autowired
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import org.springframework.test.web.reactive.server.WebTestClient
MockBean(RawCosvFileRepository::class),
MockBean(CosvFileRepository::class),
MockBean(BlockingBridge::class),
MockBean(VulnerabilityGeneratedIdRepository::class),
MockBean(CosvGeneratedIdRepository::class),
)
@AutoConfigureWebTestClient
class LnkUserOrganizationControllerTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ import java.util.concurrent.TimeUnit
MockBean(RawCosvFileRepository::class),
MockBean(CosvFileRepository::class),
MockBean(BlockingBridge::class),
MockBean(VulnerabilityGeneratedIdRepository::class),
MockBean(CosvGeneratedIdRepository::class),
)
@AutoConfigureWebTestClient
@Suppress("UnsafeCallOnNullableType")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import reactor.util.function.Tuples
MockBean(RawCosvFileRepository::class),
MockBean(CosvFileRepository::class),
MockBean(BlockingBridge::class),
MockBean(VulnerabilityGeneratedIdRepository::class),
MockBean(CosvGeneratedIdRepository::class),
)
@AutoConfigureWebTestClient
class PermissionControllerTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package com.saveourtool.save.utils

import com.saveourtool.save.entities.vulnerability.*
import com.saveourtool.save.entities.vulnerability.VulnerabilityDto.Companion.vulnerabilityPrefixes
import com.saveourtool.save.info.UserInfo

import com.saveourtool.osv4k.*
Expand All @@ -21,15 +22,27 @@ private val timelineEntryTypeMapping = mapOf(
VulnerabilityDateType.INTRODUCED to TimelineEntryType.introduced,
)

val vulnerabilityPrefixes = listOf(
"CVE-",
)

typealias ManualCosvSchema = CosvSchema<Unit, Unit, Unit, Unit>

/**
* @return Save's contributors
*/
fun CosvSchema<*, *, *, *>.getSaveContributes(): List<UserInfo> = credits
?.flatMap { credit -> credit.contact.orEmpty() }
?.mapNotNull { it.asSaveContribute() }
.orEmpty()

/**
* @return save's contributor
*/
fun Credit.asSaveContribute(): UserInfo? = contact
?.filter { it.startsWith(SAVEOURTOOL_PROFILE_PREFIX) }
?.map { it.removePrefix(SAVEOURTOOL_PROFILE_PREFIX) }
?.map { UserInfo(it) }
.orEmpty()
?.singleOrNull()

/**
* @return [Credit]
Expand Down Expand Up @@ -85,15 +98,6 @@ fun CosvSchema<*, *, *, *>.getLanguage(): VulnerabilityLanguage? = affected?.fir
fun CosvSchema<*, *, *, *>.getRelatedLink(): String? = references
?.filter { it.type == ReferenceType.WEB }?.map { it.url }?.firstOrNull()

/**
* @return Severity for a single progress
*/
fun Float.asSeverity(): Severity = Severity(
type = SeverityType.CVSS_V3,
score = "N/A",
scoreNum = toString(),
)

private fun LocalDateTime.asVulnerabilityDateDto(cosvId: String, type: VulnerabilityDateType) = VulnerabilityDateDto(
date = this,
type = type,
Expand All @@ -108,3 +112,12 @@ private fun TimelineEntry.asVulnerabilityDateDto(cosvId: String) = value.asVulne
TimelineEntryType.disclosed -> VulnerabilityDateType.DISCLOSED
}
)

/**
* Validation of [identifier]
*
* @param identifier
* @return true if [identifier] is empty (our own should be set on backend)
* or starts with one of [vulnerabilityPrefixes] (reused existed identifier), false otherwise
*/
fun validateIdentifier(identifier: String) = identifier.isEmpty() || vulnerabilityPrefixes.any { identifier.startsWith(it) }
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import javax.persistence.Entity
* Entity for generated id of vulnerability
*/
@Entity
class VulnerabilityGeneratedId : BaseEntityWithDate() {
class CosvGeneratedId : BaseEntityWithDate() {
/**
* @return Vulnerability identifier for saved entity
*/
Expand Down
Loading

0 comments on commit 91114b2

Please sign in to comment.