Compare commits

...

1 Commits

16 changed files with 274 additions and 67 deletions

View File

@ -837,4 +837,4 @@
}
}
]
}
}

View File

@ -11,4 +11,4 @@ class UserAccountDetailsService : ReactiveUserDetailsService {
override fun findByUsername(username: String): Mono<UserDetails> {
return Appuser().toMono()
}
}
}

View File

@ -63,10 +63,10 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-actuator")
/*implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
api("org.springframework.security:spring-security-jwt:1.1.1.RELEASE")*/
api("org.springframework.security:spring-security-jwt:1.1.1.RELEASE")
/* Reporting */
implementation("net.sf.jasperreports:jasperreports:6.20.0")

View File

@ -15,12 +15,12 @@
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8888/auth/realms/c4po_realm_local/.well-known/openid-configuration",
"raw": "http://localhost:8080/auth/realms/c4po_realm_local/.well-known/openid-configuration",
"protocol": "http",
"host": [
"localhost"
],
"port": "8888",
"port": "8080",
"path": [
"auth",
"realms",
@ -75,12 +75,12 @@
]
},
"url": {
"raw": "http://localhost:8888/auth/realms/c4po_realm_local/protocol/openid-connect/token",
"raw": "http://localhost:8080/auth/realms/c4po_realm_local/protocol/openid-connect/token",
"protocol": "http",
"host": [
"localhost"
],
"port": "8888",
"port": "8080",
"path": [
"auth",
"realms",

View File

@ -1,12 +1,11 @@
package com.securityc4po.reporting.configuration.security
class Appuser {}
/*import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
import java.util.stream.Collectors
class Appuser internal constructor(val token: String) : UserDetails {
class Appuser internal constructor() : UserDetails {
override fun getAuthorities(): Collection<GrantedAuthority> {
return listOf("user").stream().map {
@ -21,7 +20,7 @@ class Appuser internal constructor(val token: String) : UserDetails {
override fun getPassword(): String {
return "n/a"
}
d
override fun getUsername(): String {
return "n/a"
}
@ -45,4 +44,4 @@ class Appuser internal constructor(val token: String) : UserDetails {
companion object {
private val ROLE_PREFIX = "ROLE_"
}
}*/
}

View File

@ -0,0 +1,69 @@
package com.securityc4po.reporting.configuration.security
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import org.springframework.core.convert.converter.Converter
import org.springframework.security.authentication.AbstractAuthenticationToken
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.oauth2.jwt.Jwt
import reactor.core.publisher.Mono
import java.util.stream.Collectors
class AppuserJwtAuthConverter(private val appuserDetailsService: UserAccountDetailsService) :
Converter<Jwt, Mono<AbstractAuthenticationToken>> {
override fun convert(jwt: Jwt): Mono<AbstractAuthenticationToken> {
val authorities = extractAuthorities(jwt)
// val sub = extractSub(jwt)
val username = extractUserName(jwt)
return appuserDetailsService
.findByUsername(username)
.map { user ->
UsernamePasswordAuthenticationToken(user, "n/a", authorities);
}
}
private fun extractSub(jwt: Jwt): String {
val sub = jwt.getClaims().get(SUB).toString()
if (sub.isEmpty() || sub.equals("null")) {
return "n/a"
}
return sub
}
private fun extractUserName(jwt: Jwt): String {
val username = jwt.getClaims().get(USERNAME).toString()
if (username.isEmpty() || username.equals("null")) {
return "n/a"
}
return username
}
private fun extractAuthorities(jwt: Jwt): Collection<GrantedAuthority> {
return this.getScopes(jwt).stream().map { authority ->
ROLE_PREFIX + authority.toUpperCase()
}.map {
SimpleGrantedAuthority(it)
}.collect(Collectors.toList())
}
private fun getScopes(jwt: Jwt): Collection<String> {
val mapper = ObjectMapper()
val scopes = jwt.getClaims().get(GROUPS_CLAIM).toString()
val roleStringValue = mapper.readTree(scopes).get("roles").toString()
val roles = mapper.readValue<Collection<String>>(roleStringValue)
if (!roles.isEmpty()) {
return roles
}
return emptyList()
}
companion object {
private val GROUPS_CLAIM = "realm_access"
private val ROLE_PREFIX = "ROLE_"
private val SUB = "sub"
private val USERNAME = "username"
}
}

View File

@ -0,0 +1,17 @@
package com.securityc4po.reporting.configuration.security
import org.modelmapper.ModelMapper
import org.modelmapper.convention.MatchingStrategies
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class ModelmapperCfg {
@Bean
fun modelMapper(): ModelMapper {
val modelMapper = ModelMapper()
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT)
return modelMapper
}
}

View File

@ -0,0 +1,14 @@
package com.securityc4po.reporting.configuration.security
import org.springframework.security.core.userdetails.ReactiveUserDetailsService
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.toMono
@Service
class UserAccountDetailsService : ReactiveUserDetailsService {
override fun findByUsername(username: String): Mono<UserDetails> {
return Appuser().toMono()
}
}

View File

@ -0,0 +1,73 @@
package com.securityc4po.reporting.configuration.security
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import org.springframework.http.HttpMethod
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.oauth2.core.OAuth2TokenValidator
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.security.oauth2.jwt.JwtValidators
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.web.cors.CorsConfiguration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
@Configuration
@ComponentScan
class WebSecurityConfiguration(private val userAccountDetailsService: UserAccountDetailsService) {
@Value("\${external.issuer-uri}")
var externalIssuerUri: String? = null
@Value("\${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
var internalIssuerUri: String? = null
@Bean
fun setSecurityWebFilterChains(http: ServerHttpSecurity): SecurityWebFilterChain {
http.cors().configurationSource {
CorsConfiguration().apply {
this.applyPermitDefaultValues()
this.addAllowedMethod(HttpMethod.DELETE)
this.addAllowedMethod(HttpMethod.PATCH)
this.addAllowedMethod(HttpMethod.POST)
this.addAllowedMethod(HttpMethod.GET)
this.addAllowedMethod(HttpMethod.PUT)
}
}
.and()
.csrf()
.disable()
.authorizeExchange()
.pathMatchers(HttpMethod.GET, "/v1/reports/**").authenticated()
.pathMatchers("/actuator/**").permitAll()
/*.pathMatchers("/docs/SecurityC4PO.html").permitAll()*/
.anyExchange().authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(appuserJwtAuthenticationConverter())
return http.build()
}
@Bean
fun appuserJwtAuthenticationConverter(): AppuserJwtAuthConverter {
return AppuserJwtAuthConverter(userAccountDetailsService)
}
@Bean
@Profile("COMPOSE")
fun jwtDecoder(): ReactiveJwtDecoder {
val jwtDecoder = ReactiveJwtDecoders.fromIssuerLocation(internalIssuerUri) as NimbusReactiveJwtDecoder
val withIssuer: OAuth2TokenValidator<Jwt> = JwtValidators.createDefaultWithIssuer(externalIssuerUri)
jwtDecoder.setJwtValidator(withIssuer)
return jwtDecoder
}
}

View File

@ -2,19 +2,19 @@ package com.securityc4po.reporting.report
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.securityc4po.reporting.configuration.security.Appuser
import com.securityc4po.reporting.extensions.getLoggerFor
import com.securityc4po.reporting.remote.APIService
import com.securityc4po.reporting.remote.model.ProjectReport
import org.apache.pdfbox.io.IOUtils
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.http.ResponseEntity.notFound
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.switchIfEmpty
import java.io.File
// import com.securityc4po.reporting.configuration.security.Appuser
// import org.springframework.security.core.annotation.AuthenticationPrincipal
@RestController
@RequestMapping("/reports")
@ -32,18 +32,16 @@ class ReportController(private val apiService: APIService, private val reportSer
"/{projectId}/pdf",
produces = [MediaType.APPLICATION_PDF_VALUE]
)
fun downloadPentestReportPDF(@PathVariable(value = "projectId") projectId: String/*, @AuthenticationPrincipal user: Appuser*/): Mono<ResponseEntity<ByteArray>> {
fun downloadPentestReportPDF(@PathVariable(value = "projectId") projectId: String, @AuthenticationPrincipal user: Appuser): Mono<ResponseEntity<ByteArray>> {
// Todo: Create Report with Jasper
// this.apiService.requestProjectDataById(projectId, user.token)
val jsonProjectReportString: String =
File("./src/test/resources/ProjectReportData.json").readText(Charsets.UTF_8)
val jsonProjectReportCollection: ProjectReport =
jacksonObjectMapper().readValue<ProjectReport>(jsonProjectReportString)
return this.reportService.createReport(jsonProjectReportCollection, "pdf").map { reportClassLoaderFilePatch ->
val reportRessourceStream = ReportController::class.java.getResourceAsStream(reportClassLoaderFilePatch)
// Todo: Fix Error with IOUtils.toByteArray(reportRessourceStream) on first start of application
val response = IOUtils.toByteArray(reportRessourceStream)
ResponseEntity.ok().body(response)
// Setup headers
return this.reportService.createReport(jsonProjectReportCollection, "pdf").map { reportClassLoaderFilePath ->
ResponseEntity.ok().body(reportClassLoaderFilePath)
}.switchIfEmpty {
Mono.just(notFound().build<ByteArray>())
}.doOnSuccess {

View File

@ -5,13 +5,16 @@ import com.securityc4po.reporting.remote.model.*
import net.sf.jasperreports.engine.*
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource
import org.apache.commons.io.FileUtils
import org.apache.pdfbox.io.MemoryUsageSetting
import org.apache.pdfbox.multipdf.PDFMergerUtility
import org.springframework.stereotype.Service
import org.springframework.util.ResourceUtils
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.toMono
import java.io.ByteArrayOutputStream
import java.io.File
@Service
/*
* This Service makes use of the created Jasper Templates
@ -23,8 +26,10 @@ class ReportService {
private val reportCoverDesignTemplate = "./src/main/resources/jasper/reports/c4po_cover.jrxml"
private val reportContentDesignTemplate = "./src/main/resources/jasper/reports/c4po_content.jrxml"
private val reportStateOfConfidentialityDesignTemplate = "./src/main/resources/jasper/reports/c4po_state_of_confidentiality.jrxml"
private val reportExecutiveSummaryDesignTemplate = "./src/main/resources/jasper/reports/c4po_executive_summary.jrxml"
private val reportStateOfConfidentialityDesignTemplate =
"./src/main/resources/jasper/reports/c4po_state_of_confidentiality.jrxml"
private val reportExecutiveSummaryDesignTemplate =
"./src/main/resources/jasper/reports/c4po_executive_summary.jrxml"
private val reportPentestsDesignTemplate = "./src/main/resources/jasper/reports/c4po_pentests.jrxml"
private val reportAppendenciesDesignTemplate = "./src/main/resources/jasper/reports/c4po_appendencies.jrxml"
@ -33,39 +38,31 @@ class ReportService {
// Path where the created Reports are saved
private val reportDestination = "./src/main/resources/jasper/reportPDFs/"
// Path where the completed Report is saved
private val reportFileDestination = "./src/main/resources/jasper/finalReport/"
// Path where the completed Report can be found by class loader
private val reportFileForClassLoader = "/jasper/finalReport/"
fun createReport(projectReportCollection: ProjectReport, reportFormat: String): Mono<String> {
val C4POPentestReport: PDFMergerUtility = PDFMergerUtility()
val reportFilePathDestination: String = reportFileDestination + projectReportCollection.title.replace(" ", "_") + "_report.pdf"
val reportFilePathForClassLoader: String = reportFileForClassLoader + projectReportCollection.title.replace(" ", "_") + "_report.pdf"
fun createReport(projectReportCollection: ProjectReport, reportFormat: String): Mono<ByteArray> {
// Setup Filepath destination
val reportFilePathDestination: String =
reportFileDestination + projectReportCollection.title.replace(" ", "_") + "_report.pdf"
// Setup PDFMergerUtility
val mergedC4POPentestReport: PDFMergerUtility = PDFMergerUtility()
// Setup ByteArrayOutputStream for "on the fly" file generation
val pdfDocOutputstream = ByteArrayOutputStream()
// Try to create report files & merge them together
return try {
// Create report files
val coverFile: File = createCover(projectReportCollection, reportFormat)
val contentFile: File = createTableOfContent(projectReportCollection, reportFormat)
val confidentialityFile: File = createStateOfConfidentiality(projectReportCollection, reportFormat)
val summaryFile: File = createExecutiveSummary(projectReportCollection, reportFormat)
val pentestFiles: List<File> = createPentestReports(projectReportCollection, reportFormat)
val appendenciesFile: File = createAppendencies(reportFormat)
// Add files to [C4POPentestReport]
C4POPentestReport.addSource(coverFile)
C4POPentestReport.addSource(contentFile)
C4POPentestReport.addSource(confidentialityFile)
C4POPentestReport.addSource(summaryFile)
// Merge every Pentestreport file in List of File
pentestFiles.forEach { pentestFile -> C4POPentestReport.addSource(pentestFile) }
C4POPentestReport.addSource(appendenciesFile)
// Save completed report
C4POPentestReport.destinationFileName = reportFilePathDestination
C4POPentestReport.mergeDocuments()
reportFilePathForClassLoader.toMono()
} catch (e: Exception) {
return createPentestReportFiles(projectReportCollection, reportFormat, mergedC4POPentestReport).collectList().map {
// Merge report files
mergedC4POPentestReport.destinationFileName = reportFilePathDestination
mergedC4POPentestReport.destinationStream = pdfDocOutputstream
mergedC4POPentestReport.mergeDocuments(MemoryUsageSetting.setupTempFileOnly())
}.flatMap {
return@flatMap Mono.just(pdfDocOutputstream.toByteArray())
}.doOnError {
logger.error("Report generation failed.")
Mono.just("")
}
}
@ -78,7 +75,32 @@ class ReportService {
} catch (e: Exception) {
logger.error("Report file cleanup failed with exception: ", e)
}
}
private fun createPentestReportFiles(
projectReportCollection: ProjectReport,
reportFormat: String,
mergedC4POPentestReport: PDFMergerUtility
): Flux<Unit> {
return Flux.just(
// Create report files
createCover(projectReportCollection, reportFormat),
createTableOfContent(projectReportCollection, reportFormat),
createStateOfConfidentiality(projectReportCollection, reportFormat),
createExecutiveSummary(projectReportCollection, reportFormat),
createPentestReports(projectReportCollection, reportFormat),
createAppendencies(reportFormat)
).map { jasperObject ->
if (jasperObject is File) {
mergedC4POPentestReport.addSource(jasperObject)
} else if (jasperObject is List<*>) {
jasperObject.forEach { jasperFile ->
if (jasperFile is File) {
mergedC4POPentestReport.addSource(jasperFile)
}
}
}
}
}
private fun createCover(projectReportCollection: ProjectReport, reportFormat: String): File {
@ -284,7 +306,7 @@ class ReportService {
// Create File
var finalFile: File = File(reportDefaultPdf)
return if (reportFormat.equals("pdf")) {
JasperExportManager.exportReportToPdfFile(jasperPrintContent,reportDestination + "D_ExecutiveSummary.pdf")
JasperExportManager.exportReportToPdfFile(jasperPrintContent, reportDestination + "D_ExecutiveSummary.pdf")
finalFile = File(reportDestination + "D_ExecutiveSummary.pdf")
finalFile
} else {
@ -311,16 +333,18 @@ class ReportService {
// val pentestCommentsDataSource =
// Setup Parameter & add Sub-datasets
val parameters = HashMap<String, Any>()
parameters["PentestFindingsDataSource"] = if (projectReportCollection.projectPentestReport[i].findings.isNotEmpty()) {
JRBeanCollectionDataSource(projectReportCollection.projectPentestReport[i].findings)
} else {
JRBeanCollectionDataSource(emptyList<Finding>())
}
parameters["PentestCommentsDataSource"] = if (projectReportCollection.projectPentestReport[i].comments.isNotEmpty()) {
JRBeanCollectionDataSource(projectReportCollection.projectPentestReport[i].comments)
} else {
JRBeanCollectionDataSource(emptyList<Comment>())
}
parameters["PentestFindingsDataSource"] =
if (projectReportCollection.projectPentestReport[i].findings.isNotEmpty()) {
JRBeanCollectionDataSource(projectReportCollection.projectPentestReport[i].findings)
} else {
JRBeanCollectionDataSource(emptyList<Finding>())
}
parameters["PentestCommentsDataSource"] =
if (projectReportCollection.projectPentestReport[i].comments.isNotEmpty()) {
JRBeanCollectionDataSource(projectReportCollection.projectPentestReport[i].comments)
} else {
JRBeanCollectionDataSource(emptyList<Comment>())
}
// Fill Reports
// Print one report for each objective and merge them together afterwards
val jasperPrintPentests: JasperPrint =

View File

@ -0,0 +1,5 @@
## IdentityProvider (Keycloak) ##
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://c4po-keycloak:8080/auth/realms/c4po_realm_local
keycloakhost=c4po-keycloak
keycloak.client.url=http://c4po-keycloak:8080
keycloak.client.realm.path=auth/realms/c4po_realm_local/

View File

@ -0,0 +1,4 @@
## IdentityProvider (Keycloak) ##
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/auth/realms/c4po_realm_local
keycloakhost=localhost
keycloak.client.url=http://localhost:8080/

View File

@ -0,0 +1,3 @@
## IdentityProvider (Keycloak) ##
keycloak.client.url=http://localhost:9999
keycloak.client.realm.path=auth/realms/c4po_realm_local/

View File

@ -20,10 +20,11 @@ api.client.findings.path=pentests/findings
api.client.comments.path=pentests/comments
## IdentityProvider (Keycloak) ##
# spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8888/auth/realms/c4po_realm_local
# keycloakhost=localhost
# keycloak.client.url=http://localhost:8888
# keycloak.client.realm.path=auth/realms/c4po_realm_local/
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/auth/realms/c4po_realm_local
external.issuer-uri=http://localhost:8080/auth/realms/c4po_realm_local
keycloakhost=localhost
keycloak.client.url=http://localhost:8080
keycloak.client.realm.path=auth/realms/c4po_realm_local/
## Total number of pentests listet in the OWASP testing guide
## https://owasp.org/www-project-web-security-testing-guide/assets/archive/OWASP_Testing_Guide_v4.pdf

File diff suppressed because one or more lines are too long