mirror of
https://github.com/ysoftdevs/odc-analyzer.git
synced 2026-05-01 04:44:23 +02:00
Added support for mail notifications and WIP JIRA export.
This commit is contained in:
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)
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user