Added support for mail notifications and WIP JIRA export.

This commit is contained in:
Šesták Vít
2016-02-12 19:50:40 +01:00
parent f4fa0ee948
commit 2fb2c3fd72
32 changed files with 728 additions and 51 deletions

View File

@@ -9,7 +9,7 @@ import play.twirl.api.Txt
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/
"([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
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}")))))
}
}
class HostnameFilter(allowedHostnames: Set[String], allowAllIps: Boolean = false) extends EssentialFilter {
override def apply(next: EssentialAction): EssentialAction = new HostnameValidatingAction(allowedHostnames, allowAllIps, next)
class HostFilter(allowedHosts: Set[String], allowAllIps: Boolean = false) extends EssentialFilter {
override def apply(next: EssentialAction): EssentialAction = new HostValidatingAction(allowedHosts, allowAllIps, next)
}
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))
}

View File

@@ -113,4 +113,8 @@ h3.library-identification{
.help{
border-left: 5px solid #00c7ff;
padding-left: 10px;
}
.projects-watching .watched{
font-weight: bold;
}

View 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)
}

View 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)
}

View File

@@ -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{}
trait BambooAuthentication{
def addAuth(request: WSRequest): WSRequest
}
class SessionIdBambooAuthentication(sessionId: String) extends BambooAuthentication{
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 {
final class BambooDownloader @Inject()(@Named("bamboo-server-url") val server: String, @Named("bamboo-authentication") auth: AtlassianAuthentication)(implicit executionContext: ExecutionContext, wSClient: WSClient) extends Downloader {
private object ArtifactKeys{
val BuildLog = "Build log"

View 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
}

View File

@@ -75,8 +75,8 @@ private final case class BadFilter(pattern: String) extends Filter{
object DependencyCheckReportsParser{
final case class ResultWithSelection(result: Result, projectsWithSelection: ProjectsWithSelection)
final case class Result(bareFlatReports: Map[String, Analysis], bareFailedAnalysises: Map[String, Throwable], projects: Projects){
lazy val projectsReportInfo = new ProjectsWithReports(projects, bareFlatReports.keySet ++ bareFailedAnalysises.keySet)
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) // TODO: consider renaming to projectsWithReports
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 allDependencies = flatReports.toSeq.flatMap(r => r._2.dependencies.map(_ -> r._1))

View 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())
}
}

View File

@@ -22,6 +22,8 @@ final case class ReportInfo(
override def hashCode(): Int = 517+fullId.hashCode
def bare = copy(subprojectNameOption = None, fullId = fullId.takeWhile(_ != '/'))
}
object ProjectsWithReports{
@@ -38,21 +40,27 @@ class ProjectsWithReports (val projects: Projects, val reports: Set[String]) {
val reportIdToReportInfo = {
val reportsMap = reports.map{ unfriendlyName =>
val (baseName, theRest) = unfriendlyName.span(_ != '/')
val removeLeadingMess = RestMessBeginRegexp.replaceAllIn(_: String, "")
val removeTrailingMess = RestMessEndRegexp.replaceAllIn(_: String, "")
val removeMess = removeLeadingMess andThen removeTrailingMess
val subProjectOption = Some(removeMess(theRest)).filter(_ != "")
unfriendlyName -> ReportInfo(
projectId = baseName,
fullId = unfriendlyName,
projectName = projects.projectMap(baseName),
subprojectNameOption = subProjectOption.orElse(Some("root project"))
)
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 removeLeadingMess = RestMessBeginRegexp.replaceAllIn(_: String, "")
val removeTrailingMess = RestMessEndRegexp.replaceAllIn(_: String, "")
val removeMess = removeLeadingMess andThen removeTrailingMess
val subProjectOption = Some(removeMess(theRest)).filter(_ != "")
ReportInfo(
projectId = baseName,
fullId = unfriendlyName,
projectName = projects.projectMap(baseName),
subprojectNameOption = subProjectOption.orElse(Some("root project"))
)
}
val ungroupedReportsInfo = reportIdToReportInfo.values.toSet
def sortedReportsInfo = ungroupedReportsInfo.toSeq.sortBy(p => p.projectName -> p.projectId -> p.subprojectNameOption)
}

View File

@@ -8,7 +8,7 @@ import models.{Library, LibraryTag}
import org.joda.time.DateTime
import play.api.i18n.MessagesApi
import play.twirl.api.Txt
import services.{LibrariesService, LibraryTagAssignmentsService, OdcService, TagsService}
import services._
import views.html.DefaultRequest
import scala.concurrent.{ExecutionContext, Future}
@@ -17,6 +17,8 @@ object Statistics{
case class LibDepStatistics(libraries: Set[(Int, Library)], dependencies: Set[GroupedDependency]){
def vulnerableRatio = vulnerableDependencies.size.toDouble / dependencies.size.toDouble
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 =>
dep.vulnerabilities.map(vuln => (vuln, dep))
).groupBy(_._1).mapValues(_.map(_._2)).map(identity)
@@ -50,6 +52,8 @@ class Statistics @Inject() (
libraryTagAssignmentsService: LibraryTagAssignmentsService,
@Named("missing-GAV-exclusions") missingGAVExclusions: MissingGavExclusions,
projects: Projects,
vulnerabilityNotificationService: VulnerabilityNotificationService,
issueTrackerServiceOption: Option[IssueTrackerService],
val env: AuthEnv
)(implicit val messagesApi: MessagesApi, executionContext: ExecutionContext) extends AuthenticatedController {
@@ -167,14 +171,19 @@ class Statistics @Inject() (
name = name,
projectsWithSelection = selection.projectsWithSelection
)))){ case (vuln, vulnerableDependencies) =>
for(
for {
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,
affectedProjects = vulnerableDependencies.flatMap(dep => dep.projects.map(proj => (proj, dep))).groupBy(_._1).mapValues(_.map(_._2)),
vulnerableDependencies = vulnerableDependencies,
affectedLibraries = plainLibs,
projectsWithSelection = selection.projectsWithSelection
projectsWithSelection = selection.projectsWithSelection,
issueOption = for{
ticket <- ticketOption
issueTrackerService <- issueTrackerServiceOption
} yield ticket -> issueTrackerService.ticketLink(ticket)
))
}

View 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
}

View 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
}

View 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)
}

View 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)
}

View File

@@ -2,7 +2,6 @@ package models
import models.profile.MappedJdbcType
import models.profile.api._
import slick.lifted.Tag
abstract sealed class LibraryType(val name: String){
override final def toString: String = name

View 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)
}

View File

@@ -1,13 +1,15 @@
import slick.lifted.TableQuery
/**
* Created by user on 8/12/15.
*/
import java.nio.file.{Paths, Files}
import scala.language.reflectiveCalls
package object models {
val profile = slick.driver.PostgresDriver
val jodaSupport = com.github.tototoshi.slick.PostgresJodaSupport
import profile.api._
import profile.MappedJdbcType
object tables {
val libraries = TableQuery[Libraries]
@@ -15,6 +17,50 @@ package object models {
val tags = TableQuery[LibraryTags]
val snoozesTable = TableQuery[Snoozes]
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"))
}*/
}
}

View File

@@ -3,14 +3,19 @@ package modules
import java.io._
import java.net.URLEncoder
import java.nio.file.{Files, Path, Paths}
import java.util.concurrent.Executors
import akka.util.ClassLoaderObjectInputStream
import com.ysoft.odc._
import controllers.MissingGavExclusions
import net.ceedubs.ficus.Ficus._
import net.ceedubs.ficus.readers.ArbitraryTypeReader._
import play.api.cache.CacheApi
import play.api.inject.{Binding, Module}
import play.api.{Configuration, Environment, Logger}
import services.IssueTrackerService
import scala.concurrent.ExecutionContext
import scala.concurrent.duration.Duration
import scala.reflect.ClassTag
import scala.util.{Failure, Success, Try}
@@ -22,6 +27,7 @@ import scala.util.{Failure, Success, Try}
* * Thread safety
* * fsync: https://stackoverflow.com/questions/4072878/i-o-concept-flush-vs-sync
* * probably not removing files that are not used for a long time
*
* @param path
*/
class FileCacheApi(path: Path) extends CacheApi{
@@ -79,7 +85,10 @@ class FileCacheApi(path: Path) extends CacheApi{
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(
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{
@@ -89,11 +98,13 @@ class ConfigModule extends Module {
},
bind[MissingGavExclusions].qualifiedWith("missing-GAV-exclusions").toInstance(MissingGavExclusions(
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("yssdc.reports.bamboo.sessionId").map{s => bind[BambooAuthentication].toInstance(new SessionIdBambooAuthentication(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.sessionId").map{s => bambooAuthentication.toInstance(new SessionIdAtlassianAuthentication(s))} ++
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)}
}

View 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")
)
}
}
}

View 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)
}
}

View 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)))
)
}

View 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)
}

View 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"
}

View 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)
}

View File

@@ -49,6 +49,7 @@
<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.vulnerableLibraries(None)">Vulnerable libraries</a></li>
<li><a href="@routes.Notifications.listProjects()">Notifications</a></li>
<li>
@for((ProjectsWithSelection(filter, projects, teams), link) <- projectsOption){
<div id="project-selector">
@@ -62,7 +63,7 @@
@for(team <- teams){
<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>
}
</ul>

View 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>
}

View File

@@ -3,7 +3,8 @@
vulnerability: Vulnerability,
affectedProjects: Map[ReportInfo, Set[GroupedDependency]],
vulnerableDependencies: Set[GroupedDependency],
affectedLibraries: Set[PlainLibraryIdentifier]
affectedLibraries: Set[PlainLibraryIdentifier],
issueOption: Option[(ExportedVulnerability[String], String)]
)(implicit header: DefaultRequest)
@section = @{views.html.genericSection("vuln")("h2") _}
@main(
@@ -13,6 +14,9 @@
@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>
}
@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") {
@views.html.vulnerability("h2", "vuln-details", vulnerability)
}

View File

@@ -75,6 +75,8 @@ libraryDependencies += "net.ceedubs" %% "ficus" % "1.1.2"
libraryDependencies += "org.owasp" % "dependency-check-core" % "1.3.0"
libraryDependencies += "com.typesafe.play" %% "play-mailer" % "3.0.1"
routesImport += "binders.QueryBinders._"
// Uncomment to use Akka

View 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
View 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"

View File

@@ -25,6 +25,11 @@ GET /stats/libraries/vulnerable controllers.Statistics.vulnerableL
GET /stats/libraries/all controllers.Statistics.allLibraries(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 /vulnerability/:name controllers.Statistics.vulnerability(name, selector: Option[String])

View File

@@ -14,12 +14,13 @@ play.crypto.secret = "{{ lookup('password', 'play_secret length=64') }}"
# ~~~~~
play.i18n.langs = [ "en" ]
play.modules.enabled += "modules.ConfigModule"
play.modules.enabled += "modules.SilhouetteModule"
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.
app{
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
}
yssdc{
cronKey="{{ lookup('cron_token', 'play_secret length=64') }}"
bamboo{
url = …
}
@@ -30,6 +31,25 @@ yssdc{
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, …}
teams = […]
exclusions{
@@ -77,7 +97,7 @@ slick.dbs.odc {
driver = "slick.driver.MySQLDriver$"
db {
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"
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 = "…"
}
}