mirror of
https://github.com/ysoftdevs/odc-analyzer.git
synced 2026-03-24 10:02:00 +01:00
Added support for mail notifications and WIP JIRA export.
This commit is contained in:
@@ -9,7 +9,7 @@ import play.twirl.api.Txt
|
|||||||
|
|
||||||
import scala.concurrent.Future
|
import scala.concurrent.Future
|
||||||
|
|
||||||
class HostnameValidatingAction(allowedHostnames: Set[String], allowAllIps: Boolean, next: EssentialAction) extends EssentialAction with Results{
|
class HostValidatingAction(allowedHosts: Set[String], allowAllIps: Boolean, next: EssentialAction) extends EssentialAction with Results{
|
||||||
|
|
||||||
private val IpAddressPatternComponent = // comes from http://www.mkyong.com/regular-expressions/how-to-validate-ip-address-with-regular-expression/
|
private val IpAddressPatternComponent = // comes from http://www.mkyong.com/regular-expressions/how-to-validate-ip-address-with-regular-expression/
|
||||||
"([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
|
"([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
|
||||||
@@ -20,17 +20,17 @@ class HostnameValidatingAction(allowedHostnames: Set[String], allowAllIps: Boole
|
|||||||
private val IpAddress = ("""^"""+IpAddressPatternComponent+"""((:[0-9]+)?)$""").r
|
private val IpAddress = ("""^"""+IpAddressPatternComponent+"""((:[0-9]+)?)$""").r
|
||||||
|
|
||||||
override def apply(request: RequestHeader): Iteratee[Array[Byte], Result] = {
|
override def apply(request: RequestHeader): Iteratee[Array[Byte], Result] = {
|
||||||
if( (allowedHostnames contains request.host) || (allowAllIps && IpAddress.findFirstMatchIn(request.host).isDefined )) next.apply(request)
|
if( (allowedHosts contains request.host) || (allowAllIps && IpAddress.findFirstMatchIn(request.host).isDefined )) next.apply(request)
|
||||||
else Iteratee.flatten(Future.successful(Done(Unauthorized(Txt(s"not allowed for host ${request.host}")))))
|
else Iteratee.flatten(Future.successful(Done(Unauthorized(Txt(s"not allowed for host ${request.host}")))))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class HostnameFilter(allowedHostnames: Set[String], allowAllIps: Boolean = false) extends EssentialFilter {
|
class HostFilter(allowedHosts: Set[String], allowAllIps: Boolean = false) extends EssentialFilter {
|
||||||
override def apply(next: EssentialAction): EssentialAction = new HostnameValidatingAction(allowedHostnames, allowAllIps, next)
|
override def apply(next: EssentialAction): EssentialAction = new HostValidatingAction(allowedHosts, allowAllIps, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Filters @Inject() (csrfFilter: CSRFFilter, configuration: Configuration) extends HttpFilters {
|
class Filters @Inject() (csrfFilter: CSRFFilter, configuration: Configuration) extends HttpFilters {
|
||||||
def filters = Seq(csrfFilter, new HostnameFilter(configuration.getString("app.hostname").toSet, allowAllIps = true))
|
def filters = Seq(csrfFilter, new HostFilter(configuration.getString("app.host").toSet, allowAllIps = true))
|
||||||
}
|
}
|
||||||
@@ -114,3 +114,7 @@ h3.library-identification{
|
|||||||
border-left: 5px solid #00c7ff;
|
border-left: 5px solid #00c7ff;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.projects-watching .watched{
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
7
app/com/ysoft/odc/Absolutizer.scala
Normal file
7
app/com/ysoft/odc/Absolutizer.scala
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package com.ysoft.odc
|
||||||
|
|
||||||
|
import play.api.mvc.Call
|
||||||
|
|
||||||
|
class Absolutizer(host: String, secure: Boolean){
|
||||||
|
def absolutize(call: Call) = call.absoluteURL(secure, host)
|
||||||
|
}
|
||||||
15
app/com/ysoft/odc/AtlassianAuthentication.scala
Normal file
15
app/com/ysoft/odc/AtlassianAuthentication.scala
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package com.ysoft.odc
|
||||||
|
|
||||||
|
import play.api.libs.ws.{WSAuthScheme, WSRequest}
|
||||||
|
|
||||||
|
trait AtlassianAuthentication{
|
||||||
|
def addAuth(request: WSRequest): WSRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
class SessionIdAtlassianAuthentication(sessionId: String) extends AtlassianAuthentication{
|
||||||
|
override def addAuth(request: WSRequest): WSRequest = request.withHeaders("Cookie" -> s"JSESSIONID=${sessionId.takeWhile(_.isLetterOrDigit)}")
|
||||||
|
}
|
||||||
|
|
||||||
|
class CredentialsAtlassianAuthentication(user: String, password: String) extends AtlassianAuthentication{
|
||||||
|
override def addAuth(request: WSRequest): WSRequest = request.withQueryString("os_authType" -> "basic").withAuth(user, password, WSAuthScheme.BASIC)
|
||||||
|
}
|
||||||
@@ -61,19 +61,8 @@ final case class ArtifactDirectory(name: String, items: Map[String, ArtifactItem
|
|||||||
}
|
}
|
||||||
final case class FlatArtifactDirectory(name: String, items: Seq[(String, String)]) extends FlatArtifactItem{}
|
final case class FlatArtifactDirectory(name: String, items: Seq[(String, String)]) extends FlatArtifactItem{}
|
||||||
|
|
||||||
trait BambooAuthentication{
|
|
||||||
def addAuth(request: WSRequest): WSRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
class SessionIdBambooAuthentication(sessionId: String) extends BambooAuthentication{
|
final class BambooDownloader @Inject()(@Named("bamboo-server-url") val server: String, @Named("bamboo-authentication") auth: AtlassianAuthentication)(implicit executionContext: ExecutionContext, wSClient: WSClient) extends Downloader {
|
||||||
override def addAuth(request: WSRequest): WSRequest = request.withHeaders("Cookie" -> s"JSESSIONID=${sessionId.takeWhile(_.isLetterOrDigit)}")
|
|
||||||
}
|
|
||||||
|
|
||||||
class CredentialsBambooAuthentication(user: String, password: String) extends BambooAuthentication{
|
|
||||||
override def addAuth(request: WSRequest): WSRequest = request.withQueryString("os_authType" -> "basic").withAuth(user, password, WSAuthScheme.BASIC)
|
|
||||||
}
|
|
||||||
|
|
||||||
final class BambooDownloader @Inject() (@Named("bamboo-server-url") val server: String, auth: BambooAuthentication)(implicit executionContext: ExecutionContext, wSClient: WSClient) extends Downloader {
|
|
||||||
|
|
||||||
private object ArtifactKeys{
|
private object ArtifactKeys{
|
||||||
val BuildLog = "Build log"
|
val BuildLog = "Build log"
|
||||||
|
|||||||
8
app/com/ysoft/odc/SetDiff.scala
Normal file
8
app/com/ysoft/odc/SetDiff.scala
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package com.ysoft.odc
|
||||||
|
|
||||||
|
class SetDiff[T](val oldSet: Set[T], val newSet: Set[T]) {
|
||||||
|
lazy val added = newSet -- oldSet
|
||||||
|
lazy val removed = oldSet -- newSet
|
||||||
|
lazy val isEmpty = newSet == oldSet
|
||||||
|
def nonEmpty = !isEmpty
|
||||||
|
}
|
||||||
@@ -75,8 +75,8 @@ private final case class BadFilter(pattern: String) extends Filter{
|
|||||||
|
|
||||||
object DependencyCheckReportsParser{
|
object DependencyCheckReportsParser{
|
||||||
final case class ResultWithSelection(result: Result, projectsWithSelection: ProjectsWithSelection)
|
final case class ResultWithSelection(result: Result, projectsWithSelection: ProjectsWithSelection)
|
||||||
final case class Result(bareFlatReports: Map[String, Analysis], bareFailedAnalysises: Map[String, Throwable], projects: Projects){
|
final case class Result(bareFlatReports: Map[String, Analysis], bareFailedAnalysises: Map[String, Throwable], projects: Projects /*TODO: maybe rename to rootProjects*/){
|
||||||
lazy val projectsReportInfo = new ProjectsWithReports(projects, bareFlatReports.keySet ++ bareFailedAnalysises.keySet)
|
lazy val projectsReportInfo = new ProjectsWithReports(projects, bareFlatReports.keySet ++ bareFailedAnalysises.keySet) // TODO: consider renaming to projectsWithReports
|
||||||
lazy val flatReports: Map[ReportInfo, Analysis] = bareFlatReports.map{case (k, v) => projectsReportInfo.reportIdToReportInfo(k) -> v}
|
lazy val flatReports: Map[ReportInfo, Analysis] = bareFlatReports.map{case (k, v) => projectsReportInfo.reportIdToReportInfo(k) -> v}
|
||||||
lazy val failedAnalysises: Map[ReportInfo, Throwable] = bareFailedAnalysises.map{case (k, v) => projectsReportInfo.reportIdToReportInfo(k) -> v}
|
lazy val failedAnalysises: Map[ReportInfo, Throwable] = bareFailedAnalysises.map{case (k, v) => projectsReportInfo.reportIdToReportInfo(k) -> v}
|
||||||
lazy val allDependencies = flatReports.toSeq.flatMap(r => r._2.dependencies.map(_ -> r._1))
|
lazy val allDependencies = flatReports.toSeq.flatMap(r => r._2.dependencies.map(_ -> r._1))
|
||||||
|
|||||||
139
app/controllers/Notifications.scala
Normal file
139
app/controllers/Notifications.scala
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
import com.ysoft.odc.{Absolutizer, SetDiff}
|
||||||
|
import controllers.Statistics.LibDepStatistics
|
||||||
|
import models.{EmailMessageId, ExportedVulnerability}
|
||||||
|
import play.api.i18n.MessagesApi
|
||||||
|
import play.api.libs.Crypto
|
||||||
|
import play.api.mvc.Action
|
||||||
|
import play.api.{Configuration, Logger}
|
||||||
|
import services.{EmailExportService, IssueTrackerService, LibrariesService, VulnerabilityNotificationService}
|
||||||
|
|
||||||
|
import scala.concurrent.Future.{successful => Fut}
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
class Notifications @Inject()(
|
||||||
|
config: Configuration,
|
||||||
|
librariesService: LibrariesService,
|
||||||
|
notificationService: VulnerabilityNotificationService,
|
||||||
|
projectReportsProvider: ProjectReportsProvider,
|
||||||
|
dependencyCheckReportsParser: DependencyCheckReportsParser,
|
||||||
|
issueTrackerServiceOption: Option[IssueTrackerService],
|
||||||
|
emailExportServiceOption: Option[EmailExportService],
|
||||||
|
absolutizer: Absolutizer,
|
||||||
|
val env: AuthEnv
|
||||||
|
)(implicit val messagesApi: MessagesApi, executionContext: ExecutionContext) extends AuthenticatedController {
|
||||||
|
|
||||||
|
private val versions = Map[String, Int]()
|
||||||
|
|
||||||
|
import secureRequestConversion._
|
||||||
|
|
||||||
|
def listProjects() = SecuredAction.async { implicit req =>
|
||||||
|
val (lastRefreshTime, resultsFuture) = projectReportsProvider.resultsForVersions(versions)
|
||||||
|
val myWatchesFuture = notificationService.watchedProjectsByUser(req.identity.loginInfo).map(_.map(_.project).toSet)
|
||||||
|
for{
|
||||||
|
(successfulReports, failedReports) <- resultsFuture
|
||||||
|
myWatches <- myWatchesFuture
|
||||||
|
} yield {
|
||||||
|
val projects = dependencyCheckReportsParser.parseReports(successfulReports).projectsReportInfo.sortedReportsInfo
|
||||||
|
Ok(views.html.notifications.index(projects, myWatches))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//@inline private def filterMissingTickets(missingTickets: Set[String]) = missingTickets take 1 // for debug purposes
|
||||||
|
@inline private def filterMissingTickets(missingTickets: Set[String]) = missingTickets // for production purposes
|
||||||
|
|
||||||
|
def notifyVulnerabilities[T](
|
||||||
|
lds: LibDepStatistics, ep: notificationService.ExportPlatform[T, _], projects: ProjectsWithReports
|
||||||
|
)(
|
||||||
|
reportVulnerability: (Vulnerability, Set[GroupedDependency]) => Future[ExportedVulnerability[T]]
|
||||||
|
)(
|
||||||
|
reportChangedProjectsForVulnerability: (Vulnerability, SetDiff[String], T) => Future[Unit]
|
||||||
|
) = {
|
||||||
|
val vulnerabilitiesByName = lds.vulnerabilitiesToDependencies.map{case (v, deps) => (v.name, (v, deps))}
|
||||||
|
for{
|
||||||
|
tickets <- ep.ticketsForVulnerabilities(lds.vulnerabilityNames)
|
||||||
|
// Check existing tickets
|
||||||
|
existingTicketsIds = tickets.values.map(_._1).toSet
|
||||||
|
ticketsById = tickets.values.map{case (id, ev) => id -> ev}.toMap
|
||||||
|
existingTicketsProjects <- ep.projectsForTickets(existingTicketsIds)
|
||||||
|
_ = Logger.warn("existingTicketsProjects for "+ep+": "+existingTicketsProjects.filter(_._2.exists(_.toLowerCase.contains("wps"))).toString)
|
||||||
|
projectUpdates <- Future.traverse(existingTicketsIds){ ticketId => // If we traversed over existingTicketsProjects, we would skip vulns with no projects
|
||||||
|
val oldProjectIdsSet = existingTicketsProjects(ticketId)
|
||||||
|
val exportedVulnerability = ticketsById(ticketId)
|
||||||
|
val vulnerabilityName = exportedVulnerability.vulnerabilityName
|
||||||
|
val newProjectIdsSet = vulnerabilitiesByName(vulnerabilityName)._2.flatMap(_.projects).map(_.fullId)
|
||||||
|
val diff = new SetDiff(oldSet = oldProjectIdsSet, newSet = newProjectIdsSet)
|
||||||
|
if(diff.nonEmpty) {
|
||||||
|
reportChangedProjectsForVulnerability(lds.vulnerabilitiesByName(vulnerabilityName), diff, exportedVulnerability.ticket).flatMap { _ =>
|
||||||
|
ep.changeProjects(ticketId, diff, projects)
|
||||||
|
}.map( _ => Some(diff))
|
||||||
|
} else {
|
||||||
|
Fut(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check new tickets
|
||||||
|
missingTickets = vulnerabilitiesByName.keySet -- tickets.keySet
|
||||||
|
newTicketIds <- Future.traverse(filterMissingTickets(missingTickets)){vulnerabilityName =>
|
||||||
|
val (vulnerability, dependencies) = vulnerabilitiesByName(vulnerabilityName)
|
||||||
|
reportVulnerability(vulnerability, dependencies).flatMap{ ticket =>
|
||||||
|
ep.addTicket(ticket, dependencies.flatMap(_.projects)).map(_ => ticket.ticket)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} yield (missingTickets, newTicketIds, projectUpdates.toSet: Set[Any])
|
||||||
|
}
|
||||||
|
|
||||||
|
def cron(key: String, purgeCache: Boolean) = Action.async{
|
||||||
|
if(Crypto.constantTimeEquals(key, config.getString("yssdc.cronKey").get)){
|
||||||
|
if(purgeCache){
|
||||||
|
projectReportsProvider.purgeCache(Map())
|
||||||
|
}
|
||||||
|
val (lastRefreshTime, resultsFuture) = projectReportsProvider.resultsForVersions(versions)
|
||||||
|
for{
|
||||||
|
(successfulReports, failedReports) <- resultsFuture
|
||||||
|
libraries <- librariesService.all
|
||||||
|
parsedReports = dependencyCheckReportsParser.parseReports(successfulReports)
|
||||||
|
lds = LibDepStatistics(dependencies = parsedReports.groupedDependencies.toSet, libraries = libraries.toSet)
|
||||||
|
issuesExportResultFuture = exportToIssueTracker(lds, parsedReports.projectsReportInfo)
|
||||||
|
mailExportResultFuture = exportToEmail(lds, parsedReports.projectsReportInfo)
|
||||||
|
(missingTickets, newTicketIds, updatedTickets) <- issuesExportResultFuture
|
||||||
|
(missingEmails, newMessageIds, updatedEmails) <- mailExportResultFuture
|
||||||
|
} yield Ok(
|
||||||
|
missingTickets.mkString("\n")+"\n\n"+newTicketIds.mkString("\n")+ updatedTickets.toString+
|
||||||
|
"\n\n" +
|
||||||
|
missingEmails.mkString("\n")+"\n\n"+newMessageIds.mkString("\n") + updatedEmails.toString
|
||||||
|
)
|
||||||
|
}else{
|
||||||
|
Fut(Unauthorized("unauthorized"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def forService[S, T](serviceOption: Option[S])(f: S => Future[(Set[String], Set[T], Set[Any])]) = serviceOption.fold(Fut((Set[String](), Set[T](), Set[Any]())))(f)
|
||||||
|
|
||||||
|
private def exportToEmail(lds: LibDepStatistics, p: ProjectsWithReports) = forService(emailExportServiceOption){ emailExportService =>
|
||||||
|
notifyVulnerabilities[EmailMessageId](lds, notificationService.mailExport, p) { (vulnerability, dependencies) =>
|
||||||
|
emailExportService.mailForVulnerability(vulnerability, dependencies).flatMap(emailExportService.sendEmail).map(id => ExportedVulnerability(vulnerability.name, EmailMessageId(id), 0))
|
||||||
|
}{ (vuln, diff, msgid) =>
|
||||||
|
emailExportService.mailForVulnerabilityProjectsChange(vuln, msgid, diff, p).flatMap(emailExportService.sendEmail).map(_ => ())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def exportToIssueTracker(lds: LibDepStatistics, p: ProjectsWithReports) = forService(issueTrackerServiceOption){ issueTrackerService =>
|
||||||
|
notifyVulnerabilities[String](lds, notificationService.issueTrackerExport, p) { (vulnerability, dependencies) =>
|
||||||
|
issueTrackerService.reportVulnerability(vulnerability)
|
||||||
|
}{ (vuln, diff, ticket) =>
|
||||||
|
Fut(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def watch(project: String) = SecuredAction.async{ implicit req =>
|
||||||
|
for( _ <-notificationService.subscribe(req.identity.loginInfo, project) ) yield Redirect(routes.Notifications.listProjects())
|
||||||
|
}
|
||||||
|
|
||||||
|
def unwatch(project: String) = SecuredAction.async{implicit req =>
|
||||||
|
for( _ <-notificationService.unsubscribe(req.identity.loginInfo, project) ) yield Redirect(routes.Notifications.listProjects())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ final case class ReportInfo(
|
|||||||
|
|
||||||
override def hashCode(): Int = 517+fullId.hashCode
|
override def hashCode(): Int = 517+fullId.hashCode
|
||||||
|
|
||||||
|
def bare = copy(subprojectNameOption = None, fullId = fullId.takeWhile(_ != '/'))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object ProjectsWithReports{
|
object ProjectsWithReports{
|
||||||
@@ -38,21 +40,27 @@ class ProjectsWithReports (val projects: Projects, val reports: Set[String]) {
|
|||||||
|
|
||||||
val reportIdToReportInfo = {
|
val reportIdToReportInfo = {
|
||||||
val reportsMap = reports.map{ unfriendlyName =>
|
val reportsMap = reports.map{ unfriendlyName =>
|
||||||
|
unfriendlyName -> parseUnfriendlyName(unfriendlyName)
|
||||||
|
}.toMap
|
||||||
|
reportsMap ++ reportsMap.values.map(r => r.projectId -> ReportInfo(projectId = r.projectId, fullId = r.projectId, subprojectNameOption = None, projectName = r.projectName))
|
||||||
|
}
|
||||||
|
|
||||||
|
def parseUnfriendlyName(unfriendlyName: String): ReportInfo = {
|
||||||
val (baseName, theRest) = unfriendlyName.span(_ != '/')
|
val (baseName, theRest) = unfriendlyName.span(_ != '/')
|
||||||
val removeLeadingMess = RestMessBeginRegexp.replaceAllIn(_: String, "")
|
val removeLeadingMess = RestMessBeginRegexp.replaceAllIn(_: String, "")
|
||||||
val removeTrailingMess = RestMessEndRegexp.replaceAllIn(_: String, "")
|
val removeTrailingMess = RestMessEndRegexp.replaceAllIn(_: String, "")
|
||||||
val removeMess = removeLeadingMess andThen removeTrailingMess
|
val removeMess = removeLeadingMess andThen removeTrailingMess
|
||||||
val subProjectOption = Some(removeMess(theRest)).filter(_ != "")
|
val subProjectOption = Some(removeMess(theRest)).filter(_ != "")
|
||||||
unfriendlyName -> ReportInfo(
|
ReportInfo(
|
||||||
projectId = baseName,
|
projectId = baseName,
|
||||||
fullId = unfriendlyName,
|
fullId = unfriendlyName,
|
||||||
projectName = projects.projectMap(baseName),
|
projectName = projects.projectMap(baseName),
|
||||||
subprojectNameOption = subProjectOption.orElse(Some("root project"))
|
subprojectNameOption = subProjectOption.orElse(Some("root project"))
|
||||||
)
|
)
|
||||||
}.toMap
|
|
||||||
reportsMap ++ reportsMap.values.map(r => r.projectId -> ReportInfo(projectId = r.projectId, fullId = r.projectId, subprojectNameOption = None, projectName = r.projectName))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val ungroupedReportsInfo = reportIdToReportInfo.values.toSet
|
val ungroupedReportsInfo = reportIdToReportInfo.values.toSet
|
||||||
|
|
||||||
|
def sortedReportsInfo = ungroupedReportsInfo.toSeq.sortBy(p => p.projectName -> p.projectId -> p.subprojectNameOption)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import models.{Library, LibraryTag}
|
|||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import play.api.i18n.MessagesApi
|
import play.api.i18n.MessagesApi
|
||||||
import play.twirl.api.Txt
|
import play.twirl.api.Txt
|
||||||
import services.{LibrariesService, LibraryTagAssignmentsService, OdcService, TagsService}
|
import services._
|
||||||
import views.html.DefaultRequest
|
import views.html.DefaultRequest
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
@@ -17,6 +17,8 @@ object Statistics{
|
|||||||
case class LibDepStatistics(libraries: Set[(Int, Library)], dependencies: Set[GroupedDependency]){
|
case class LibDepStatistics(libraries: Set[(Int, Library)], dependencies: Set[GroupedDependency]){
|
||||||
def vulnerableRatio = vulnerableDependencies.size.toDouble / dependencies.size.toDouble
|
def vulnerableRatio = vulnerableDependencies.size.toDouble / dependencies.size.toDouble
|
||||||
lazy val vulnerabilities: Set[Vulnerability] = dependencies.flatMap(_.vulnerabilities)
|
lazy val vulnerabilities: Set[Vulnerability] = dependencies.flatMap(_.vulnerabilities)
|
||||||
|
lazy val vulnerabilitiesByName = vulnerabilities.map(v => v.name -> v).toMap
|
||||||
|
lazy val vulnerabilityNames = vulnerabilities.map(_.name)
|
||||||
lazy val vulnerabilitiesToDependencies: Map[Vulnerability, Set[GroupedDependency]] = vulnerableDependencies.flatMap(dep =>
|
lazy val vulnerabilitiesToDependencies: Map[Vulnerability, Set[GroupedDependency]] = vulnerableDependencies.flatMap(dep =>
|
||||||
dep.vulnerabilities.map(vuln => (vuln, dep))
|
dep.vulnerabilities.map(vuln => (vuln, dep))
|
||||||
).groupBy(_._1).mapValues(_.map(_._2)).map(identity)
|
).groupBy(_._1).mapValues(_.map(_._2)).map(identity)
|
||||||
@@ -50,6 +52,8 @@ class Statistics @Inject() (
|
|||||||
libraryTagAssignmentsService: LibraryTagAssignmentsService,
|
libraryTagAssignmentsService: LibraryTagAssignmentsService,
|
||||||
@Named("missing-GAV-exclusions") missingGAVExclusions: MissingGavExclusions,
|
@Named("missing-GAV-exclusions") missingGAVExclusions: MissingGavExclusions,
|
||||||
projects: Projects,
|
projects: Projects,
|
||||||
|
vulnerabilityNotificationService: VulnerabilityNotificationService,
|
||||||
|
issueTrackerServiceOption: Option[IssueTrackerService],
|
||||||
val env: AuthEnv
|
val env: AuthEnv
|
||||||
)(implicit val messagesApi: MessagesApi, executionContext: ExecutionContext) extends AuthenticatedController {
|
)(implicit val messagesApi: MessagesApi, executionContext: ExecutionContext) extends AuthenticatedController {
|
||||||
|
|
||||||
@@ -167,14 +171,19 @@ class Statistics @Inject() (
|
|||||||
name = name,
|
name = name,
|
||||||
projectsWithSelection = selection.projectsWithSelection
|
projectsWithSelection = selection.projectsWithSelection
|
||||||
)))){ case (vuln, vulnerableDependencies) =>
|
)))){ case (vuln, vulnerableDependencies) =>
|
||||||
for(
|
for {
|
||||||
plainLibs <- librariesService.byPlainLibraryIdentifiers(vulnerableDependencies.flatMap(_.plainLibraryIdentifiers)).map(_.keySet)
|
plainLibs <- librariesService.byPlainLibraryIdentifiers(vulnerableDependencies.flatMap(_.plainLibraryIdentifiers)).map(_.keySet)
|
||||||
) yield Ok(views.html.statistics.vulnerability(
|
ticketOption <- vulnerabilityNotificationService.issueTrackerExport.ticketForVulnerability(name)
|
||||||
|
} yield Ok(views.html.statistics.vulnerability(
|
||||||
vulnerability = vuln,
|
vulnerability = vuln,
|
||||||
affectedProjects = vulnerableDependencies.flatMap(dep => dep.projects.map(proj => (proj, dep))).groupBy(_._1).mapValues(_.map(_._2)),
|
affectedProjects = vulnerableDependencies.flatMap(dep => dep.projects.map(proj => (proj, dep))).groupBy(_._1).mapValues(_.map(_._2)),
|
||||||
vulnerableDependencies = vulnerableDependencies,
|
vulnerableDependencies = vulnerableDependencies,
|
||||||
affectedLibraries = plainLibs,
|
affectedLibraries = plainLibs,
|
||||||
projectsWithSelection = selection.projectsWithSelection
|
projectsWithSelection = selection.projectsWithSelection,
|
||||||
|
issueOption = for{
|
||||||
|
ticket <- ticketOption
|
||||||
|
issueTrackerService <- issueTrackerServiceOption
|
||||||
|
} yield ticket -> issueTrackerService.ticketLink(ticket)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
5
app/models/EmailMessageId.scala
Normal file
5
app/models/EmailMessageId.scala
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
case class EmailMessageId(messageId: String) extends AnyVal {
|
||||||
|
def validIdOption = Some(messageId).filterNot(_ == "") // Prevents using invalid empty string when using mock
|
||||||
|
}
|
||||||
9
app/models/ExportPlatformTables.scala
Normal file
9
app/models/ExportPlatformTables.scala
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import models.profile.api._
|
||||||
|
|
||||||
|
abstract class ExportPlatformTables[T, U] private[models] () {
|
||||||
|
val tickets: TableQuery[_ <: ExportedVulnerabilities[T, U]]
|
||||||
|
val projects: TableQuery[_ <: ExportedVulnerabilityProjects]
|
||||||
|
def schema = tickets.schema ++ projects.schema
|
||||||
|
}
|
||||||
19
app/models/ExportedVulnerability.scala
Normal file
19
app/models/ExportedVulnerability.scala
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import models.profile.api._
|
||||||
|
import slick.lifted.{MappedProjection, Tag}
|
||||||
|
|
||||||
|
|
||||||
|
case class ExportedVulnerability[T] (vulnerabilityName: String, ticket: T, ticketFormatVersion: Int/*, maintainedAutomatically: Boolean*/)
|
||||||
|
|
||||||
|
abstract class ExportedVulnerabilities[T, U](tag: Tag, tableNamePart: String) extends Table[(Int, ExportedVulnerability[T])](tag, s"exported_${tableNamePart}_vulnerabilities"){
|
||||||
|
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
|
||||||
|
def vulnerabilityName = column[String]("vulnerability_name")
|
||||||
|
def ticketFormatVersion = column[Int]("ticket_format_version")
|
||||||
|
//def maintainedAutomatically = column[Boolean]("maintained_automatically")
|
||||||
|
|
||||||
|
def base: MappedProjection[ExportedVulnerability[T], U]// = (vulnerabilityName, ticket, ticketFormatVersion) <> ((ExportedVulnerability.apply[T] _).tupled, ExportedVulnerability.unapply[T])
|
||||||
|
def * = (id, base)
|
||||||
|
|
||||||
|
def idx_vulnerabilityName = index(s"idx_${tableName}_vulnerabilityName", vulnerabilityName, unique = true)
|
||||||
|
}
|
||||||
15
app/models/ExportedVulnerabilityProject.scala
Normal file
15
app/models/ExportedVulnerabilityProject.scala
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import models.profile.MappedJdbcType
|
||||||
|
import models.profile.api._
|
||||||
|
import slick.lifted.ProvenShape
|
||||||
|
|
||||||
|
case class ExportedVulnerabilityProject(exportedVulnerabilityId: Int, projectFullId: String)
|
||||||
|
|
||||||
|
abstract class ExportedVulnerabilityProjects(tag: Tag, tableNamePart: String) extends Table[ExportedVulnerabilityProject](tag, s"exported_${tableNamePart}_vulnerability_projects"){
|
||||||
|
def exportedVulnerabilityId = column[Int]("exported_vulnerability_id")
|
||||||
|
def fullProjectId = column[String]("full_project_id")
|
||||||
|
def idx_all = index(s"idx_${tableName}_all", shape, unique = true)
|
||||||
|
private def shape = (exportedVulnerabilityId, fullProjectId)
|
||||||
|
override def * = shape <> (ExportedVulnerabilityProject.tupled, ExportedVulnerabilityProject.unapply)
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package models
|
|||||||
|
|
||||||
import models.profile.MappedJdbcType
|
import models.profile.MappedJdbcType
|
||||||
import models.profile.api._
|
import models.profile.api._
|
||||||
import slick.lifted.Tag
|
|
||||||
|
|
||||||
abstract sealed class LibraryType(val name: String){
|
abstract sealed class LibraryType(val name: String){
|
||||||
override final def toString: String = name
|
override final def toString: String = name
|
||||||
|
|||||||
22
app/models/VulnerabilitySubscription.scala
Normal file
22
app/models/VulnerabilitySubscription.scala
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import models.profile.api._
|
||||||
|
import slick.lifted.Tag
|
||||||
|
import com.mohiva.play.silhouette.api.LoginInfo
|
||||||
|
|
||||||
|
case class VulnerabilitySubscription(user: LoginInfo, project: String)
|
||||||
|
|
||||||
|
class LoginInfoColumns(prefix: String, table: Table[_]) {
|
||||||
|
import table.column
|
||||||
|
def providerId = column[String](s"${prefix}_provider_id")
|
||||||
|
def providerKey = column[String](s"${prefix}_provider_key")
|
||||||
|
def apply() = (providerId, providerKey) <> (LoginInfo.tupled, LoginInfo.unapply)
|
||||||
|
def === (other: LoginInfo): Rep[Boolean] = (providerId === other.providerID) && (providerKey === other.providerKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
class VulnerabilitySubscriptions(tag: Tag) extends Table[VulnerabilitySubscription](tag, "vulnerability_subscription"){
|
||||||
|
val user = new LoginInfoColumns("subscriber", this)
|
||||||
|
def project = column[String]("project")
|
||||||
|
def * = (user(), project) <> (VulnerabilitySubscription.tupled, VulnerabilitySubscription.unapply)
|
||||||
|
def idx = index("all", (user(), project), unique = true)
|
||||||
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
import slick.lifted.TableQuery
|
|
||||||
|
|
||||||
/**
|
import java.nio.file.{Paths, Files}
|
||||||
* Created by user on 8/12/15.
|
|
||||||
*/
|
import scala.language.reflectiveCalls
|
||||||
|
|
||||||
package object models {
|
package object models {
|
||||||
|
|
||||||
val profile = slick.driver.PostgresDriver
|
val profile = slick.driver.PostgresDriver
|
||||||
|
|
||||||
val jodaSupport = com.github.tototoshi.slick.PostgresJodaSupport
|
val jodaSupport = com.github.tototoshi.slick.PostgresJodaSupport
|
||||||
|
import profile.api._
|
||||||
|
import profile.MappedJdbcType
|
||||||
|
|
||||||
|
|
||||||
object tables {
|
object tables {
|
||||||
val libraries = TableQuery[Libraries]
|
val libraries = TableQuery[Libraries]
|
||||||
@@ -15,6 +17,50 @@ package object models {
|
|||||||
val tags = TableQuery[LibraryTags]
|
val tags = TableQuery[LibraryTags]
|
||||||
val snoozesTable = TableQuery[Snoozes]
|
val snoozesTable = TableQuery[Snoozes]
|
||||||
val authTokens = TableQuery[CookieAuthenticators]
|
val authTokens = TableQuery[CookieAuthenticators]
|
||||||
|
val vulnerabilitySubscriptions = TableQuery[VulnerabilitySubscriptions]
|
||||||
|
|
||||||
|
val issueTrackerExportTables = new ExportPlatformTables[String, (String, String, Int)](){
|
||||||
|
val tableNamePart = "issue_tracker"
|
||||||
|
class IssueTrackerVulnerabilities(tag: Tag) extends ExportedVulnerabilities[String, (String, String, Int)](tag, tableNamePart){
|
||||||
|
def ticket = column[String]("ticket")
|
||||||
|
override def base = (vulnerabilityName, ticket, ticketFormatVersion) <> ((ExportedVulnerability.apply[String] _).tupled, ExportedVulnerability.unapply[String])
|
||||||
|
def idx_ticket = index("idx_ticket", ticket, unique = true)
|
||||||
|
}
|
||||||
|
class IssueTrackerVulnerabilityProject(tag: Tag) extends ExportedVulnerabilityProjects(tag, tableNamePart)
|
||||||
|
override val tickets = TableQuery[IssueTrackerVulnerabilities]
|
||||||
|
override val projects: profile.api.TableQuery[_ <: ExportedVulnerabilityProjects] = TableQuery[IssueTrackerVulnerabilityProject]
|
||||||
|
}
|
||||||
|
type EmailExportedVulnerabilitiesShape = (String, EmailMessageId, Int)
|
||||||
|
val mailExportTables = new ExportPlatformTables[EmailMessageId, EmailExportedVulnerabilitiesShape](){
|
||||||
|
val tableNamePart = "email"
|
||||||
|
class EmailExportedVulnerabilities(tag: Tag) extends ExportedVulnerabilities[EmailMessageId, EmailExportedVulnerabilitiesShape](tag, tableNamePart){
|
||||||
|
private implicit val mmiMapper = MappedJdbcType.base[EmailMessageId, String](_.messageId, EmailMessageId)
|
||||||
|
def messageId = column[EmailMessageId]("message_id") // Unlike ticket, message id is not required to be unique in order to handle some edge cases like play.mailer.mock = true
|
||||||
|
override def base = (vulnerabilityName, messageId, ticketFormatVersion) <> ( (ExportedVulnerability.apply[EmailMessageId] _).tupled, ExportedVulnerability.unapply[EmailMessageId])
|
||||||
|
}
|
||||||
|
class EmailVulnerabilityProject(tag: Tag) extends ExportedVulnerabilityProjects(tag, tableNamePart)
|
||||||
|
|
||||||
|
override val projects = TableQuery[EmailVulnerabilityProject]
|
||||||
|
override val tickets = TableQuery[EmailExportedVulnerabilities]
|
||||||
|
}
|
||||||
|
|
||||||
|
/*{
|
||||||
|
import profile.SchemaDescription
|
||||||
|
val schema = Seq[Any{def schema: SchemaDescription}](
|
||||||
|
vulnerabilitySubscriptions, issueTrackerExportTables, mailExportTables
|
||||||
|
).map(_.schema).foldLeft(profile.DDL(Seq(), Seq()))(_ ++ _)
|
||||||
|
|
||||||
|
val sql = Seq(
|
||||||
|
"# --- !Ups",
|
||||||
|
schema.createStatements.toSeq.map(_+";").mkString("\n").dropWhile(_ == "\n"),
|
||||||
|
"",
|
||||||
|
"# --- !Downs",
|
||||||
|
schema.dropStatements.toSeq.map(_+";").mkString("\n").dropWhile(_ == "\n"),
|
||||||
|
"\n"
|
||||||
|
).mkString("\n")
|
||||||
|
Files.write(Paths.get("conf/evolutions/default/6.sql"), sql.getBytes("utf-8"))
|
||||||
|
}*/
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,19 @@ package modules
|
|||||||
import java.io._
|
import java.io._
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.nio.file.{Files, Path, Paths}
|
import java.nio.file.{Files, Path, Paths}
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
import akka.util.ClassLoaderObjectInputStream
|
import akka.util.ClassLoaderObjectInputStream
|
||||||
import com.ysoft.odc._
|
import com.ysoft.odc._
|
||||||
import controllers.MissingGavExclusions
|
import controllers.MissingGavExclusions
|
||||||
|
import net.ceedubs.ficus.Ficus._
|
||||||
|
import net.ceedubs.ficus.readers.ArbitraryTypeReader._
|
||||||
import play.api.cache.CacheApi
|
import play.api.cache.CacheApi
|
||||||
import play.api.inject.{Binding, Module}
|
import play.api.inject.{Binding, Module}
|
||||||
import play.api.{Configuration, Environment, Logger}
|
import play.api.{Configuration, Environment, Logger}
|
||||||
|
import services.IssueTrackerService
|
||||||
|
|
||||||
|
import scala.concurrent.ExecutionContext
|
||||||
import scala.concurrent.duration.Duration
|
import scala.concurrent.duration.Duration
|
||||||
import scala.reflect.ClassTag
|
import scala.reflect.ClassTag
|
||||||
import scala.util.{Failure, Success, Try}
|
import scala.util.{Failure, Success, Try}
|
||||||
@@ -22,6 +27,7 @@ import scala.util.{Failure, Success, Try}
|
|||||||
* * Thread safety
|
* * Thread safety
|
||||||
* * fsync: https://stackoverflow.com/questions/4072878/i-o-concept-flush-vs-sync
|
* * fsync: https://stackoverflow.com/questions/4072878/i-o-concept-flush-vs-sync
|
||||||
* * probably not removing files that are not used for a long time
|
* * probably not removing files that are not used for a long time
|
||||||
|
*
|
||||||
* @param path
|
* @param path
|
||||||
*/
|
*/
|
||||||
class FileCacheApi(path: Path) extends CacheApi{
|
class FileCacheApi(path: Path) extends CacheApi{
|
||||||
@@ -80,6 +86,9 @@ class FileCacheApi(path: Path) extends CacheApi{
|
|||||||
|
|
||||||
class ConfigModule extends Module {
|
class ConfigModule extends Module {
|
||||||
|
|
||||||
|
private val bambooAuthentication = bind[AtlassianAuthentication].qualifiedWith("bamboo-authentication")
|
||||||
|
//private val jiraAuthentication = bind[AtlassianAuthentication].qualifiedWith("jira-authentication")
|
||||||
|
|
||||||
override def bindings(environment: Environment, configuration: Configuration): Seq[Binding[_]] = Seq(
|
override def bindings(environment: Environment, configuration: Configuration): Seq[Binding[_]] = Seq(
|
||||||
bind[String].qualifiedWith("bamboo-server-url").toInstance(configuration.getString("yssdc.bamboo.url").getOrElse(sys.error("Key yssdc.bamboo.url is not set"))),
|
bind[String].qualifiedWith("bamboo-server-url").toInstance(configuration.getString("yssdc.bamboo.url").getOrElse(sys.error("Key yssdc.bamboo.url is not set"))),
|
||||||
configuration.getString("yssdc.reports.provider") match{
|
configuration.getString("yssdc.reports.provider") match{
|
||||||
@@ -89,11 +98,13 @@ class ConfigModule extends Module {
|
|||||||
},
|
},
|
||||||
bind[MissingGavExclusions].qualifiedWith("missing-GAV-exclusions").toInstance(MissingGavExclusions(
|
bind[MissingGavExclusions].qualifiedWith("missing-GAV-exclusions").toInstance(MissingGavExclusions(
|
||||||
configuration.getStringSeq("yssdc.exclusions.missingGAV.bySha1").getOrElse(Seq()).toSet.map(Exclusion))
|
configuration.getStringSeq("yssdc.exclusions.missingGAV.bySha1").getOrElse(Seq()).toSet.map(Exclusion))
|
||||||
)
|
),
|
||||||
|
bind[ExecutionContext].qualifiedWith("email-sending").toInstance(ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor()))
|
||||||
) ++
|
) ++
|
||||||
|
configuration.underlying.getAs[Absolutizer]("app").map(a => bind[Absolutizer].toInstance(a)) ++
|
||||||
configuration.getString("play.cache.path").map(cachePath => bind[CacheApi].toInstance(new FileCacheApi(Paths.get(cachePath)))) ++
|
configuration.getString("play.cache.path").map(cachePath => bind[CacheApi].toInstance(new FileCacheApi(Paths.get(cachePath)))) ++
|
||||||
configuration.getString("yssdc.reports.bamboo.sessionId").map{s => bind[BambooAuthentication].toInstance(new SessionIdBambooAuthentication(s))} ++
|
configuration.getString("yssdc.reports.bamboo.sessionId").map{s => bambooAuthentication.toInstance(new SessionIdAtlassianAuthentication(s))} ++
|
||||||
configuration.getString("yssdc.reports.bamboo.user").map{u => bind[BambooAuthentication].toInstance(new CredentialsBambooAuthentication(u, configuration.getString("yssdc.reports.bamboo.password").get))} ++
|
configuration.getString("yssdc.reports.bamboo.user").map{u => bambooAuthentication.toInstance(new CredentialsAtlassianAuthentication(u, configuration.getString("yssdc.reports.bamboo.password").get))} ++
|
||||||
configuration.getString("yssdc.reports.path").map{s => bind[String].qualifiedWith("reports-path").toInstance(s)}
|
configuration.getString("yssdc.reports.path").map{s => bind[String].qualifiedWith("reports-path").toInstance(s)}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
33
app/modules/EmailExportModule.scala
Normal file
33
app/modules/EmailExportModule.scala
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package modules
|
||||||
|
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
|
import com.google.inject.{AbstractModule, Provides}
|
||||||
|
import com.ysoft.odc.Absolutizer
|
||||||
|
import net.ceedubs.ficus.Ficus._
|
||||||
|
import net.codingwell.scalaguice.ScalaModule
|
||||||
|
import play.api.Configuration
|
||||||
|
import play.api.libs.mailer.MailerClient
|
||||||
|
import services.{EmailExportService, VulnerabilityNotificationService}
|
||||||
|
|
||||||
|
import scala.concurrent.ExecutionContext
|
||||||
|
|
||||||
|
class EmailExportModule extends AbstractModule with ScalaModule{
|
||||||
|
override def configure(): Unit = {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
def provideIssueTrackerOption(conf: Configuration, mailerClient: MailerClient, notificationService: VulnerabilityNotificationService, absolutizer: Absolutizer, @Named("email-sending") emailSendingExecutionContext: ExecutionContext)(implicit executionContext: ExecutionContext): Option[EmailExportService] = {
|
||||||
|
println(s"emailSendingExecutionContext = $emailSendingExecutionContext")
|
||||||
|
conf.getConfig("yssdc.export.email").map{c =>
|
||||||
|
new EmailExportService(
|
||||||
|
from = c.underlying.as[String]("from"),
|
||||||
|
mailerClient = mailerClient,
|
||||||
|
emailSendingExecutionContext = emailSendingExecutionContext,
|
||||||
|
absolutizer = absolutizer,
|
||||||
|
notificationService = notificationService,
|
||||||
|
nobodyInterestedContact = c.underlying.as[String]("noSubscriberContact")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
app/modules/IssueTrackerExportModule.scala
Normal file
39
app/modules/IssueTrackerExportModule.scala
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package modules
|
||||||
|
|
||||||
|
import com.google.inject.{AbstractModule, Provides}
|
||||||
|
import com.ysoft.odc.{Absolutizer, CredentialsAtlassianAuthentication}
|
||||||
|
import net.ceedubs.ficus.Ficus._
|
||||||
|
import net.ceedubs.ficus.readers.ArbitraryTypeReader._
|
||||||
|
import net.codingwell.scalaguice.ScalaModule
|
||||||
|
import play.api.Configuration
|
||||||
|
import play.api.libs.ws.WSClient
|
||||||
|
import services.{IssueTrackerService, JiraIssueTrackerService}
|
||||||
|
|
||||||
|
import scala.concurrent.ExecutionContext
|
||||||
|
|
||||||
|
class IssueTrackerExportModule extends AbstractModule with ScalaModule{
|
||||||
|
override def configure(): Unit = {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
def provideIssueTrackerOption(conf: Configuration, absolutizer: Absolutizer)(implicit executionContext: ExecutionContext, wSClient: WSClient): Option[IssueTrackerService] = {
|
||||||
|
conf.getConfig("yssdc.export.issueTracker").map(issueTrackerConfiguration(absolutizer))
|
||||||
|
}
|
||||||
|
|
||||||
|
private def issueTrackerConfiguration(absolutizer: Absolutizer)(conf: Configuration)(implicit executionContext: ExecutionContext, wSClient: WSClient): IssueTrackerService = conf.getString("provider") match{
|
||||||
|
case Some("jira") =>
|
||||||
|
conf.getString("authentication.type") match {
|
||||||
|
case Some("credentials") =>
|
||||||
|
case other => sys.error("unknown authentication type: "+other)
|
||||||
|
}
|
||||||
|
new JiraIssueTrackerService(
|
||||||
|
absolutizer = absolutizer,
|
||||||
|
server = conf.underlying.as[String]("server"),
|
||||||
|
projectId = conf.underlying.as[Int]("projectId"),
|
||||||
|
vulnerabilityIssueType = conf.underlying.as[Int]("vulnerabilityIssueType"),
|
||||||
|
atlassianAuthentication = conf.underlying.as[CredentialsAtlassianAuthentication]("authentication")
|
||||||
|
)
|
||||||
|
case other => sys.error("unknown provider for issue tracker: "+other)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
64
app/services/EmailExportService.scala
Normal file
64
app/services/EmailExportService.scala
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import java.util.NoSuchElementException
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
|
import com.ysoft.odc.{SetDiff, Absolutizer}
|
||||||
|
import controllers._
|
||||||
|
import models.EmailMessageId
|
||||||
|
import play.api.libs.mailer.{MailerClient, Email}
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
class EmailExportService(from: String, nobodyInterestedContact: String, mailerClient: MailerClient, notificationService: VulnerabilityNotificationService, emailSendingExecutionContext: ExecutionContext, absolutizer: Absolutizer)(implicit executionContext: ExecutionContext) {
|
||||||
|
|
||||||
|
def recipientsForProjects(projects: Set[ReportInfo]) = for{
|
||||||
|
recipients <- notificationService.getRecipientsForProjects(projects)
|
||||||
|
} yield {
|
||||||
|
recipients.map(_.providerKey) match { // TODO: get the email in a cleaner way
|
||||||
|
case Seq() => Seq(nobodyInterestedContact) -> false
|
||||||
|
case other => other -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def mailForVulnerabilityProjectsChange(vuln: Vulnerability, emailMessageId: EmailMessageId, diff: SetDiff[String], projects: ProjectsWithReports) = {
|
||||||
|
def showProjects(s: Set[String]) = s.map(p =>
|
||||||
|
"* " + (try{
|
||||||
|
friendlyProjectName(projects.parseUnfriendlyName(p))
|
||||||
|
}catch{ // It might fail on project that has been removed
|
||||||
|
case e: NoSuchElementException => s"unknown project $p"
|
||||||
|
})
|
||||||
|
).mkString("\n")
|
||||||
|
for{
|
||||||
|
(recipients, somebodySubscribed) <- recipientsForProjects(diff.added.map(projects.parseUnfriendlyName))
|
||||||
|
} yield Email(
|
||||||
|
subject = s"[${vuln.name}] Modified vulnerability${if(!somebodySubscribed) ", nobody is subscribed for that" else "" }",
|
||||||
|
from = from,
|
||||||
|
to = Seq(),
|
||||||
|
replyTo = emailMessageId.validIdOption,
|
||||||
|
headers = emailMessageId.validIdOption.map("References" -> _).toSeq,
|
||||||
|
bcc = recipients,
|
||||||
|
bodyText = Some(
|
||||||
|
"New projects affected by the vulnerability: \n"+showProjects(diff.added) + "\n\n" +
|
||||||
|
"Projects no longer affected by the vulnerability: \n"+showProjects(diff.removed) + "\n\n" +
|
||||||
|
s"More details: "+absolutizer.absolutize(routes.Statistics.vulnerability(vuln.name, None))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def sendEmail(email: Email): Future[String] = Future{
|
||||||
|
mailerClient.send(email)
|
||||||
|
}(emailSendingExecutionContext)
|
||||||
|
|
||||||
|
def mailForVulnerability(vulnerability: Vulnerability, dependencies: Set[GroupedDependency]) = for {
|
||||||
|
(recipientEmails, somebodySubscribed) <- recipientsForProjects(dependencies.flatMap(_.projects))
|
||||||
|
} yield Email(
|
||||||
|
subject = s"[${vulnerability.name}] New vulnerability${if(!somebodySubscribed) ", nobody is subscribed for that" else "" }",
|
||||||
|
from = from,
|
||||||
|
to = Seq(),
|
||||||
|
bcc = recipientEmails,
|
||||||
|
bodyText = Some(vulnerability.description + "\n\n" + s"More details: "+absolutizer.absolutize(routes.Statistics.vulnerability(vulnerability.name, None)))
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
12
app/services/IssueTrackerService.scala
Normal file
12
app/services/IssueTrackerService.scala
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import controllers.Vulnerability
|
||||||
|
import models.ExportedVulnerability
|
||||||
|
|
||||||
|
import scala.concurrent.Future
|
||||||
|
|
||||||
|
trait IssueTrackerService {
|
||||||
|
def reportVulnerability(vulnerability: Vulnerability): Future[ExportedVulnerability[String]]
|
||||||
|
def ticketLink(ticket: String): String
|
||||||
|
def ticketLink(ticket: ExportedVulnerability[String]): String = ticketLink(ticket.ticket)
|
||||||
|
}
|
||||||
60
app/services/JiraIssueTrackerService.scala
Normal file
60
app/services/JiraIssueTrackerService.scala
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
import com.google.inject.name.Named
|
||||||
|
import com.ysoft.odc.{Absolutizer, AtlassianAuthentication}
|
||||||
|
import controllers.{Vulnerability, routes}
|
||||||
|
import models.ExportedVulnerability
|
||||||
|
import play.api.libs.json.Json.JsValueWrapper
|
||||||
|
import play.api.libs.json.{JsObject, Json}
|
||||||
|
import play.api.libs.ws.{WS, WSClient}
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
private case class JiraNewIssueResponse(id: String, key: String, self: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* status: WIP
|
||||||
|
* It basically works, but there is much to be discussed and implemented.
|
||||||
|
*/
|
||||||
|
class JiraIssueTrackerService @Inject() (absolutizer: Absolutizer, @Named("jira-server") server: String, @Named("jira-project-id") projectId: Int, @Named("jira-vulnerability-issue-type") vulnerabilityIssueType: Int, @Named("jira-authentication") atlassianAuthentication: AtlassianAuthentication)(implicit executionContext: ExecutionContext, wSClient: WSClient) extends IssueTrackerService{
|
||||||
|
private def jiraUrl(url: String) = atlassianAuthentication.addAuth(WS.clientUrl(url))
|
||||||
|
|
||||||
|
private val formatVersion = 1
|
||||||
|
|
||||||
|
override def reportVulnerability(vulnerability: Vulnerability): Future[ExportedVulnerability[String]] = jiraUrl(server+"/rest/api/2/issue").post(Json.obj(
|
||||||
|
"fields" -> (extractInitialFields(vulnerability) ++ extractManagedFields(vulnerability))
|
||||||
|
)).map(response => // returns responses like {"id":"1234","key":"PROJ-6","self":"https://…/rest/api/2/issue/1234"}
|
||||||
|
try{
|
||||||
|
val issueInfo = Json.reads[JiraNewIssueResponse].reads(response.json).get
|
||||||
|
ExportedVulnerability(vulnerabilityName = vulnerability.name, ticket = issueInfo.key, ticketFormatVersion = formatVersion)
|
||||||
|
}catch{
|
||||||
|
case e:Throwable=>sys.error("bad data: "+response.body)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
private def extractInitialFields(vulnerability: Vulnerability): JsObject = Json.obj(
|
||||||
|
"project" -> Json.obj(
|
||||||
|
"id" -> projectId.toString
|
||||||
|
),
|
||||||
|
"summary" -> s"${vulnerability.name} – ${vulnerability.cweOption.map(_ + ": ").getOrElse("")}${vulnerability.description.take(50)}…"
|
||||||
|
)
|
||||||
|
|
||||||
|
private def extractManagedFields(vulnerability: Vulnerability): JsObject = Json.obj(
|
||||||
|
"issuetype" -> Json.obj(
|
||||||
|
"id" -> vulnerabilityIssueType.toString
|
||||||
|
),
|
||||||
|
"description" -> extractDescription(vulnerability)
|
||||||
|
// TODO: add affected releases
|
||||||
|
// TODO: add affected projects
|
||||||
|
//"customfield_10100" -> Json.arr("xxxx")
|
||||||
|
)
|
||||||
|
|
||||||
|
private def extractDescription(vulnerability: Vulnerability): JsValueWrapper = {
|
||||||
|
vulnerability.description + "\n\n" + s"Details: ${absolutizer.absolutize(routes.Statistics.vulnerability(vulnerability.name, None))}"
|
||||||
|
}
|
||||||
|
|
||||||
|
override def ticketLink(ticket: String): String = s"$server/browse/$ticket"
|
||||||
|
|
||||||
|
}
|
||||||
68
app/services/VulnerabilityNotificationService.scala
Normal file
68
app/services/VulnerabilityNotificationService.scala
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import com.google.inject.Inject
|
||||||
|
import com.mohiva.play.silhouette.api.LoginInfo
|
||||||
|
import com.ysoft.odc.SetDiff
|
||||||
|
import controllers.{ProjectsWithReports, ReportInfo}
|
||||||
|
import models._
|
||||||
|
import play.api.db.slick.{HasDatabaseConfigProvider, DatabaseConfigProvider}
|
||||||
|
|
||||||
|
import scala.collection.immutable.Iterable
|
||||||
|
import scala.concurrent.{Future, ExecutionContext}
|
||||||
|
|
||||||
|
class VulnerabilityNotificationService @Inject() (protected val dbConfigProvider: DatabaseConfigProvider)(implicit executionContext: ExecutionContext) extends HasDatabaseConfigProvider[models.profile.type]{
|
||||||
|
import dbConfig.driver.api._
|
||||||
|
|
||||||
|
import models.tables
|
||||||
|
import models.tables.vulnerabilitySubscriptions
|
||||||
|
|
||||||
|
def watchedProjectsByUser(identity: LoginInfo) = db.run(vulnerabilitySubscriptions.filter(_.user === identity).result)
|
||||||
|
def subscribe(user: LoginInfo, project: String) = db.run(vulnerabilitySubscriptions += VulnerabilitySubscription(user = user, project = project))
|
||||||
|
|
||||||
|
def unsubscribe(user: LoginInfo, project: String) = db.run(vulnerabilitySubscriptions.filter(vs => vs.user === user && vs.project === project).delete)
|
||||||
|
|
||||||
|
def getRecipientsForProjects(projects: Set[ReportInfo]) = {
|
||||||
|
val bareProjects = projects.map(_.bare)
|
||||||
|
val expandedProjects = projects ++ bareProjects
|
||||||
|
val relevantFullIds = expandedProjects.map(_.fullId)
|
||||||
|
db.run(vulnerabilitySubscriptions.filter(_.project inSet relevantFullIds).map(_.user()).result)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExportPlatform[T, U] private[VulnerabilityNotificationService] (ept: ExportPlatformTables[T, U]) {
|
||||||
|
def changeProjects(ticketId: Int, diff: SetDiff[String], projects: ProjectsWithReports) = db.run(
|
||||||
|
DBIO.seq(
|
||||||
|
ept.projects.filter(_.exportedVulnerabilityId === ticketId).delete,
|
||||||
|
ept.projects ++= diff.newSet.map(fullId => ExportedVulnerabilityProject(ticketId, fullId)).toSet
|
||||||
|
).transactionally
|
||||||
|
)
|
||||||
|
|
||||||
|
def projectsForTickets(ticketsIds: Set[Int]): Future[Map[Int, Set[String]]] = db.run(
|
||||||
|
ept.projects.filter(_.exportedVulnerabilityId inSet ticketsIds).result
|
||||||
|
).map{_.groupBy(_.exportedVulnerabilityId).mapValues(_.map(_.projectFullId).toSet).map(identity).withDefaultValue(Set())}
|
||||||
|
|
||||||
|
def ticketsForVulnerabilities(vulnerabilities: Traversable[String]) = db.run(
|
||||||
|
ept.tickets.filter(_.vulnerabilityName inSet vulnerabilities).result
|
||||||
|
).map(_.map{ rec =>
|
||||||
|
rec._2.vulnerabilityName -> rec
|
||||||
|
}.toMap)
|
||||||
|
|
||||||
|
def ticketForVulnerability(vulnerabilityName: String) = db.run(
|
||||||
|
ept.tickets.filter(_.vulnerabilityName === vulnerabilityName).map(_.base).result
|
||||||
|
).map(_.headOption)
|
||||||
|
|
||||||
|
|
||||||
|
def addTicket(vulnerabilityTicket: ExportedVulnerability[T], projects: Set[ReportInfo]): Future[Any] = db.run(
|
||||||
|
(
|
||||||
|
ept.tickets.map(_.base).returning(ept.tickets.map(_.id)) += vulnerabilityTicket
|
||||||
|
).flatMap( id =>
|
||||||
|
ept.projects ++= projects.map(ri => ExportedVulnerabilityProject(id, ri.fullId)).toSet
|
||||||
|
).transactionally
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
val issueTrackerExport = new ExportPlatform(tables.issueTrackerExportTables)
|
||||||
|
|
||||||
|
val mailExport = new ExportPlatform(tables.mailExportTables)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -49,6 +49,7 @@
|
|||||||
<li><a href="@routes.Statistics.basic(None)">Tag statistics</a></li>
|
<li><a href="@routes.Statistics.basic(None)">Tag statistics</a></li>
|
||||||
<li><a href="@routes.Statistics.vulnerabilities(None, None)">Vulnerabilities</a></li>
|
<li><a href="@routes.Statistics.vulnerabilities(None, None)">Vulnerabilities</a></li>
|
||||||
<li><a href="@routes.Statistics.vulnerableLibraries(None)">Vulnerable libraries</a></li>
|
<li><a href="@routes.Statistics.vulnerableLibraries(None)">Vulnerable libraries</a></li>
|
||||||
|
<li><a href="@routes.Notifications.listProjects()">Notifications</a></li>
|
||||||
<li>
|
<li>
|
||||||
@for((ProjectsWithSelection(filter, projects, teams), link) <- projectsOption){
|
@for((ProjectsWithSelection(filter, projects, teams), link) <- projectsOption){
|
||||||
<div id="project-selector">
|
<div id="project-selector">
|
||||||
@@ -62,7 +63,7 @@
|
|||||||
@for(team <- teams){
|
@for(team <- teams){
|
||||||
<li><a href="@link(Some("team:"+team.id))" title="team leader: @team.leader">@team.name</a></li>
|
<li><a href="@link(Some("team:"+team.id))" title="team leader: @team.leader">@team.name</a></li>
|
||||||
}
|
}
|
||||||
@for(report <- projects.ungroupedReportsInfo.toSeq.sortBy(p => p.projectName -> p.projectId -> p.subprojectNameOption)){
|
@for(report <- projects.sortedReportsInfo){
|
||||||
<li@if(report.subprojectNameOption.isEmpty){ class="base-project"}><a href="@link(Some("project:"+report.fullId))">@friendlyProjectName(report)</a></li>
|
<li@if(report.subprojectNameOption.isEmpty){ class="base-project"}><a href="@link(Some("project:"+report.fullId))">@friendlyProjectName(report)</a></li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
32
app/views/notifications/index.scala.html
Normal file
32
app/views/notifications/index.scala.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
@(projects: Seq[ReportInfo], watchedProjects: Set[String])(implicit req: DefaultRequest)
|
||||||
|
@import helper._
|
||||||
|
@button(action: Call)(label: String) = {
|
||||||
|
@form(action, 'style -> "display: inline-block"){
|
||||||
|
@CSRF.formField
|
||||||
|
<button type="submit" class="btn">@label</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@main("Watch projects"){
|
||||||
|
<ul class="projects-watching">
|
||||||
|
@for(
|
||||||
|
project <- projects;
|
||||||
|
fullId = project.fullId;
|
||||||
|
isWatchedThroughParent = project.subprojectNameOption.isDefined && (watchedProjects contains project.projectId);
|
||||||
|
isWatchedDirectly = watchedProjects contains fullId;
|
||||||
|
isWatched = isWatchedDirectly || isWatchedThroughParent
|
||||||
|
){
|
||||||
|
<li @if(isWatched){class="watched"}>
|
||||||
|
@friendlyProjectName(project)
|
||||||
|
@if(isWatchedThroughParent){
|
||||||
|
<button disabled class="btn">unwatch</button>
|
||||||
|
<span class="badge">watched through parent</span>
|
||||||
|
}else{
|
||||||
|
@if(isWatchedDirectly){
|
||||||
|
@button(routes.Notifications.unwatch(fullId))("unwatch")
|
||||||
|
}else{
|
||||||
|
@button(routes.Notifications.watch(fullId))("watch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
@@ -3,7 +3,8 @@
|
|||||||
vulnerability: Vulnerability,
|
vulnerability: Vulnerability,
|
||||||
affectedProjects: Map[ReportInfo, Set[GroupedDependency]],
|
affectedProjects: Map[ReportInfo, Set[GroupedDependency]],
|
||||||
vulnerableDependencies: Set[GroupedDependency],
|
vulnerableDependencies: Set[GroupedDependency],
|
||||||
affectedLibraries: Set[PlainLibraryIdentifier]
|
affectedLibraries: Set[PlainLibraryIdentifier],
|
||||||
|
issueOption: Option[(ExportedVulnerability[String], String)]
|
||||||
)(implicit header: DefaultRequest)
|
)(implicit header: DefaultRequest)
|
||||||
@section = @{views.html.genericSection("vuln")("h2") _}
|
@section = @{views.html.genericSection("vuln")("h2") _}
|
||||||
@main(
|
@main(
|
||||||
@@ -13,6 +14,9 @@
|
|||||||
@if(projectsWithSelection.isProjectSpecified){
|
@if(projectsWithSelection.isProjectSpecified){
|
||||||
<div class="alert alert-warning">The vulnerability details are limited to some subset of projects.<br><a class="btn btn-default" href="@routes.Statistics.vulnerability(vulnerability.name, None)">Show it for all projects!</a></div>
|
<div class="alert alert-warning">The vulnerability details are limited to some subset of projects.<br><a class="btn btn-default" href="@routes.Statistics.vulnerability(vulnerability.name, None)">Show it for all projects!</a></div>
|
||||||
}
|
}
|
||||||
|
@for((ticket, issueLink) <- issueOption){
|
||||||
|
<a class="btn btn-block btn-primary" href="@issueLink">Issue in your issue tracker: @ticket.ticket</a>
|
||||||
|
}
|
||||||
@section("details", "Vulnerability details") {
|
@section("details", "Vulnerability details") {
|
||||||
@views.html.vulnerability("h2", "vuln-details", vulnerability)
|
@views.html.vulnerability("h2", "vuln-details", vulnerability)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ libraryDependencies += "net.ceedubs" %% "ficus" % "1.1.2"
|
|||||||
|
|
||||||
libraryDependencies += "org.owasp" % "dependency-check-core" % "1.3.0"
|
libraryDependencies += "org.owasp" % "dependency-check-core" % "1.3.0"
|
||||||
|
|
||||||
|
libraryDependencies += "com.typesafe.play" %% "play-mailer" % "3.0.1"
|
||||||
|
|
||||||
routesImport += "binders.QueryBinders._"
|
routesImport += "binders.QueryBinders._"
|
||||||
|
|
||||||
// Uncomment to use Akka
|
// Uncomment to use Akka
|
||||||
|
|||||||
20
conf/evolutions/default/6.sql
Normal file
20
conf/evolutions/default/6.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# --- !Ups
|
||||||
|
create table "vulnerability_subscription" ("subscriber_provider_id" VARCHAR NOT NULL,"subscriber_provider_key" VARCHAR NOT NULL,"project" VARCHAR NOT NULL);
|
||||||
|
create unique index "all" on "vulnerability_subscription" ("subscriber_provider_id","subscriber_provider_key","project");
|
||||||
|
create table "exported_issue_tracker_vulnerabilities" ("id" SERIAL NOT NULL PRIMARY KEY,"vulnerability_name" VARCHAR NOT NULL,"ticket" VARCHAR NOT NULL,"ticket_format_version" INTEGER NOT NULL);
|
||||||
|
create unique index "idx_exported_issue_tracker_vulnerabilities_vulnerabilityName" on "exported_issue_tracker_vulnerabilities" ("vulnerability_name");
|
||||||
|
create unique index "idx_ticket" on "exported_issue_tracker_vulnerabilities" ("ticket");
|
||||||
|
create table "exported_issue_tracker_vulnerability_projects" ("exported_vulnerability_id" INTEGER NOT NULL,"full_project_id" VARCHAR NOT NULL);
|
||||||
|
create unique index "idx_exported_issue_tracker_vulnerability_projects_all" on "exported_issue_tracker_vulnerability_projects" ("exported_vulnerability_id","full_project_id");
|
||||||
|
create table "exported_email_vulnerabilities" ("id" SERIAL NOT NULL PRIMARY KEY,"vulnerability_name" VARCHAR NOT NULL,"message_id" VARCHAR NOT NULL,"ticket_format_version" INTEGER NOT NULL);
|
||||||
|
create unique index "idx_exported_email_vulnerabilities_vulnerabilityName" on "exported_email_vulnerabilities" ("vulnerability_name");
|
||||||
|
create table "exported_email_vulnerability_projects" ("exported_vulnerability_id" INTEGER NOT NULL,"full_project_id" VARCHAR NOT NULL);
|
||||||
|
create unique index "idx_exported_email_vulnerability_projects_all" on "exported_email_vulnerability_projects" ("exported_vulnerability_id","full_project_id");
|
||||||
|
|
||||||
|
# --- !Downs
|
||||||
|
drop table "exported_email_vulnerability_projects";
|
||||||
|
drop table "exported_email_vulnerabilities";
|
||||||
|
drop table "exported_issue_tracker_vulnerability_projects";
|
||||||
|
drop table "exported_issue_tracker_vulnerabilities";
|
||||||
|
drop table "vulnerability_subscription";
|
||||||
|
|
||||||
4
conf/reference.conf
Normal file
4
conf/reference.conf
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
play.modules.enabled += "modules.ConfigModule"
|
||||||
|
play.modules.enabled += "modules.SilhouetteModule"
|
||||||
|
play.modules.enabled += "modules.IssueTrackerExportModule"
|
||||||
|
play.modules.enabled += "modules.EmailExportModule"
|
||||||
@@ -25,6 +25,11 @@ GET /stats/libraries/vulnerable controllers.Statistics.vulnerableL
|
|||||||
GET /stats/libraries/all controllers.Statistics.allLibraries(selector: Option[String])
|
GET /stats/libraries/all controllers.Statistics.allLibraries(selector: Option[String])
|
||||||
GET /stats/libraries/gavs controllers.Statistics.allGavs(selector: Option[String])
|
GET /stats/libraries/gavs controllers.Statistics.allGavs(selector: Option[String])
|
||||||
|
|
||||||
|
GET /notifications controllers.Notifications.listProjects()
|
||||||
|
POST /notifications/watch controllers.Notifications.watch(project: String)
|
||||||
|
POST /notifications/unwatch controllers.Notifications.unwatch(project: String)
|
||||||
|
GET /notifications/cron/:key controllers.Notifications.cron(key: String, purgeCache: Boolean ?= true)
|
||||||
|
|
||||||
GET /libraries/vulnerabilities controllers.Statistics.searchVulnerableSoftware(versionlessCpes: Seq[String], versionOption: Option[String])
|
GET /libraries/vulnerabilities controllers.Statistics.searchVulnerableSoftware(versionlessCpes: Seq[String], versionOption: Option[String])
|
||||||
|
|
||||||
GET /vulnerability/:name controllers.Statistics.vulnerability(name, selector: Option[String])
|
GET /vulnerability/:name controllers.Statistics.vulnerability(name, selector: Option[String])
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ play.crypto.secret = "{{ lookup('password', 'play_secret length=64') }}"
|
|||||||
# ~~~~~
|
# ~~~~~
|
||||||
play.i18n.langs = [ "en" ]
|
play.i18n.langs = [ "en" ]
|
||||||
|
|
||||||
play.modules.enabled += "modules.ConfigModule"
|
app{
|
||||||
play.modules.enabled += "modules.SilhouetteModule"
|
host=… # You have to configure the host there. If you don't do so, all accesses via host will be prohibited. This is a protection against DNS rebind attacks.
|
||||||
|
secure = true # Use true iff you use HTTPS
|
||||||
app.hostname=… # You have to configure the hostname there. If you don't do so, all accesses via hostname will be prohibited. This is a protection against DNS rebind attacks.
|
}
|
||||||
|
|
||||||
yssdc{
|
yssdc{
|
||||||
|
cronKey="{{ lookup('cron_token', 'play_secret length=64') }}"
|
||||||
bamboo{
|
bamboo{
|
||||||
url = …
|
url = …
|
||||||
}
|
}
|
||||||
@@ -30,6 +31,25 @@ yssdc{
|
|||||||
password = …
|
password = …
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export{
|
||||||
|
# Optional section: export to issue tracker
|
||||||
|
issueTracker{
|
||||||
|
provider: "jira"
|
||||||
|
server: "http://…"
|
||||||
|
projectId = 10000
|
||||||
|
vulnerabilityIssueType = 10100
|
||||||
|
authentication {
|
||||||
|
type = "credentials"
|
||||||
|
user = "…"
|
||||||
|
password = "…"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Optional section: email notifications
|
||||||
|
email{
|
||||||
|
from = "info@example.com"
|
||||||
|
noSubscriberContact = "foobar@example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
projects = {jobId:humanReadableName, …}
|
projects = {jobId:humanReadableName, …}
|
||||||
teams = […]
|
teams = […]
|
||||||
exclusions{
|
exclusions{
|
||||||
@@ -77,7 +97,7 @@ slick.dbs.odc {
|
|||||||
driver = "slick.driver.MySQLDriver$"
|
driver = "slick.driver.MySQLDriver$"
|
||||||
db {
|
db {
|
||||||
url = "jdbc:mysql://127.0.0.1/dependencycheck"
|
url = "jdbc:mysql://127.0.0.1/dependencycheck"
|
||||||
# Those credentials are default in ODC (but you might have changed them):
|
# These credentials are default in ODC (but you might have changed them):
|
||||||
user = "dcuser"
|
user = "dcuser"
|
||||||
password = "DC-Pass1337!"
|
password = "DC-Pass1337!"
|
||||||
}
|
}
|
||||||
@@ -113,3 +133,11 @@ silhouette {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
play{
|
||||||
|
# needed if you want this app to send emails
|
||||||
|
mailer{
|
||||||
|
//mock = true # If mock is true, mails are not actually sent, but just logged.
|
||||||
|
host = "…"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user