mirror of
https://github.com/ysoftdevs/odc-analyzer.git
synced 2026-04-30 12:24:26 +02:00
Initial commit
This commit is contained in:
293
app/controllers/Application.scala
Normal file
293
app/controllers/Application.scala
Normal file
@@ -0,0 +1,293 @@
|
||||
package controllers
|
||||
|
||||
import java.sql.BatchUpdateException
|
||||
|
||||
import com.github.nscala_time.time.Imports._
|
||||
import com.google.inject.Inject
|
||||
import com.google.inject.name.Named
|
||||
import models._
|
||||
import play.api.Logger
|
||||
import play.api.data.Forms._
|
||||
import play.api.data._
|
||||
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
|
||||
import play.api.http.ContentTypes
|
||||
import play.api.i18n.{I18nSupport, MessagesApi}
|
||||
import play.api.libs.json._
|
||||
import play.api.mvc._
|
||||
import play.api.routing.JavaScriptReverseRouter
|
||||
import play.twirl.api.Txt
|
||||
import services.{LibrariesService, LibraryTagAssignmentsService, TagsService}
|
||||
import views.html.DefaultRequest
|
||||
|
||||
import scala.collection.immutable.SortedMap
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.Future
|
||||
|
||||
object ApplicationFormats{
|
||||
implicit val libraryTagPairFormat = Json.format[LibraryTagPair]
|
||||
implicit val libraryTagAssignmentFormat = Json.format[LibraryTagAssignment]
|
||||
//implicit val libraryTypeFormat = Json.format[LibraryType]
|
||||
//implicit val plainLibraryIdentifierFormat = Json.format[PlainLibraryIdentifier]
|
||||
//implicit val libraryFormat = Json.format[Library]
|
||||
implicit val libraryTagFormat = Json.format[LibraryTag]
|
||||
}
|
||||
|
||||
object export {
|
||||
import ApplicationFormats._
|
||||
final case class AssignedTag(name: String, contextDependent: Boolean)
|
||||
final case class TaggedLibrary(identifier: String, classified: Boolean, tags: Seq[AssignedTag]){
|
||||
def toLibrary = Library(plainLibraryIdentifier = PlainLibraryIdentifier.fromString(identifier), classified = classified)
|
||||
}
|
||||
final case class Export(libraryMapping: Seq[TaggedLibrary], tags: Seq[LibraryTag])
|
||||
implicit val assignedTagFormats = Json.format[AssignedTag]
|
||||
implicit val taggedLibraryFormats = Json.format[TaggedLibrary]
|
||||
implicit val exportFormats = Json.format[Export]
|
||||
}
|
||||
|
||||
|
||||
class Application @Inject() (
|
||||
reportsParser: DependencyCheckReportsParser,
|
||||
reportsProcessor: DependencyCheckReportsProcessor,
|
||||
projectReportsProvider: ProjectReportsProvider,
|
||||
@Named("missing-GAV-exclusions") missingGAVExclusions: MissingGavExclusions,
|
||||
tagsService: TagsService,
|
||||
librariesService: LibrariesService,
|
||||
libraryTagAssignmentsService: LibraryTagAssignmentsService,
|
||||
protected val dbConfigProvider: DatabaseConfigProvider,
|
||||
val messagesApi: MessagesApi,
|
||||
val env: AuthEnv
|
||||
) extends AuthenticatedController with HasDatabaseConfigProvider[models.profile.type]{
|
||||
|
||||
import ApplicationFormats._
|
||||
import dbConfig.driver.api._
|
||||
import models.tables.snoozesTable
|
||||
import reportsProcessor.processResults
|
||||
|
||||
import secureRequestConversion._
|
||||
|
||||
val dateFormatter = DateTimeFormat.forPattern("dd-MM-yyyy")
|
||||
val emptySnoozeForm = Form(mapping(
|
||||
"until" -> text.transform(LocalDate.parse(_, dateFormatter), (_: LocalDate).toString(dateFormatter)).verifying("Must be a date in the future", _ > LocalDate.now),
|
||||
//"snoozed_object_identifier" -> text,
|
||||
"reason" -> text(minLength = 3, maxLength = 255)
|
||||
)(ObjectSnooze.apply)(ObjectSnooze.unapply))
|
||||
|
||||
def loadSnoozes() = {
|
||||
val now = LocalDate.now
|
||||
import models.jodaSupport._
|
||||
for{
|
||||
bareSnoozes <- db.run(snoozesTable.filter(_.until > now).result) : Future[Seq[(Int, Snooze)]]
|
||||
snoozes = bareSnoozes.groupBy(_._2.snoozedObjectId).mapValues(ss => SnoozeInfo(emptySnoozeForm, ss.sortBy(_._2.until))).map(identity)
|
||||
} yield snoozes.withDefaultValue(SnoozeInfo(emptySnoozeForm, Seq()))
|
||||
}
|
||||
|
||||
def purgeCache(versions: Map[String, Int], next: String) = Action {
|
||||
projectReportsProvider.purgeCache(versions)
|
||||
next match {
|
||||
case "index" => Redirect(routes.Application.index(versions))
|
||||
case _ => Ok(Txt("CACHE PURGED"))
|
||||
}
|
||||
}
|
||||
|
||||
def index(versions: Map[String, Int]) = ReadAction.async{ implicit req =>
|
||||
loadSnoozes() flatMap { snoozes =>
|
||||
indexPage(versions)(snoozes, securedRequestToUserAwareRequest(req))
|
||||
}
|
||||
}
|
||||
|
||||
def indexPage(requiredVersions: Map[String, Int])(implicit snoozes: SnoozesInfo, requestHeader: DefaultRequest) = {
|
||||
val (lastRefreshTimeFuture, resultsFuture) = projectReportsProvider.resultsForVersions(requiredVersions)
|
||||
processResults(resultsFuture, requiredVersions).flatMap{ case (vulnerableDependencies, allWarnings, groupedDependencies) =>
|
||||
Logger.debug("indexPage: Got results")
|
||||
//val unclassifiedDependencies = groupedDependencies.filterNot(ds => MissingGAVExclusions.exists(_.matches(ds))).filterNot(_.identifiers.exists(_.isClassifiedInSet(classifiedSet)))
|
||||
for{
|
||||
knownDependencies <- librariesService.allBase
|
||||
_ = Logger.debug("indexPage: #1")
|
||||
includedDependencies = groupedDependencies.filterNot(missingGAVExclusions.isExcluded)
|
||||
_ = Logger.debug("indexPage: #2")
|
||||
unknownDependencies = includedDependencies.flatMap(_.identifiers.flatMap(_.toLibraryIdentifierOption)).toSet -- knownDependencies.map(_.plainLibraryIdentifier).toSet
|
||||
_ = Logger.debug("indexPage: #3")
|
||||
_ <- librariesService.insertMany(unknownDependencies.map(Library(_, classified = false)))
|
||||
_ = Logger.debug("indexPage: #3")
|
||||
unclassifiedDependencies <- librariesService.unclassified
|
||||
_ = Logger.debug("indexPage: #4")
|
||||
allTags <- tagsService.all
|
||||
_ = Logger.debug("indexPage: #6")
|
||||
allTagsMap = allTags.toMap
|
||||
_ = Logger.debug("indexPage: #7")
|
||||
tagsWithWarning = allTags.collect(Function.unlift{case (id, t: LibraryTag) => t.warningOrder.map(_ => (id, t))}).sortBy(_._2.warningOrder)
|
||||
_ = Logger.debug("indexPage: #8")
|
||||
librariesForTagsWithWarningUnsorted <- librariesService.librariesForTags(tagsWithWarning.map(_._1))
|
||||
_ = Logger.debug("indexPage: #9")
|
||||
librariesForTagsWithWarning = SortedMap(librariesForTagsWithWarningUnsorted.groupBy(_._1).toSeq.map{case (tagId, lr) => (tagId, allTagsMap(tagId)) -> lr.map(_._2) } : _*)(Ordering.by(t => (t._2.warningOrder, t._1)))
|
||||
_ = Logger.debug("indexPage: #10")
|
||||
relatedDependenciesTags <- librariesService.byTags(unclassifiedDependencies.map(_._1).toSet ++ librariesForTagsWithWarning.values.flatten.map(_._1).toSet)
|
||||
_ = Logger.debug("indexPage: #11")
|
||||
lastRefreshTime <- lastRefreshTimeFuture
|
||||
} yield {
|
||||
Logger.debug("indexPage: Got all ingredients")
|
||||
Ok(views.html.index(
|
||||
vulnerableDependencies = vulnerableDependencies,
|
||||
warnings = allWarnings,
|
||||
librariesForTagsWithWarning = librariesForTagsWithWarning,
|
||||
unclassifiedDependencies = unclassifiedDependencies,
|
||||
groupedDependencies = groupedDependencies,
|
||||
dependenciesForLibraries = groupedDependencies.flatMap(group =>
|
||||
group.identifiers.flatMap(_.toLibraryIdentifierOption).map(_ -> group)
|
||||
).groupBy(_._1).mapValues(_.map(_._2).toSet).map(identity),
|
||||
allTags = allTags,
|
||||
relatedDependenciesTags = relatedDependenciesTags,
|
||||
lastRefreshTime = lastRefreshTime,
|
||||
versions = requiredVersions
|
||||
))
|
||||
}
|
||||
} recover {
|
||||
case e: BatchUpdateException =>
|
||||
throw e.getNextException
|
||||
}
|
||||
}
|
||||
|
||||
implicit class AddAdjustToMap[K, V](m: Map[K, V]){
|
||||
def adjust(k: K)(f: V => V) = m.updated(k, f(m(k)))
|
||||
}
|
||||
|
||||
def snooze(id: String, versions: Map[String, Int]) = AdminAction.async { implicit req =>
|
||||
loadSnoozes().flatMap{ loadedSnoozes =>
|
||||
val snoozes = loadedSnoozes.adjust(id){_.adjustForm(_.bindFromRequest()(req))}
|
||||
snoozes(id).form.fold(
|
||||
f => indexPage(Map())(snoozes, securedRequestToUserAwareRequest(req)),
|
||||
snooze => for {
|
||||
_ <- db.run(snoozesTable.map(_.base) += snooze.toSnooze(id))
|
||||
} yield Redirect(routes.Application.index(versions).withFragment(id))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def unsnooze(snoozeId: Int, versions: Map[String, Int]) = AdminAction.async { implicit req =>
|
||||
(db.run(snoozesTable.filter(_.id === snoozeId).map(_.base).result).map(_.headOption): Future[Option[Snooze]]).flatMap {
|
||||
case Some(snooze) =>
|
||||
for(_ <- db.run(snoozesTable.filter(_.id === snoozeId).delete)) yield Redirect(routes.Application.index(versions).withFragment(snooze.snoozedObjectId))
|
||||
case None => Future.successful(NotFound(Txt("Unknown snoozeId")))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move import/export to a separate controller
|
||||
def tagsExport = Action.async {
|
||||
import export._
|
||||
for{
|
||||
tags <- tagsService.all.map(_.toMap)
|
||||
lta <- libraryTagAssignmentsService.byLibrary
|
||||
libs <- librariesService.touched(lta.keySet)
|
||||
} yield {
|
||||
val libraryMapping = (libs: Seq[(Int, Library)]).sortBy(_._2.plainLibraryIdentifier.toString).map { case (id, l) =>
|
||||
val assignments: Seq[LibraryTagAssignment] = lta(id)
|
||||
TaggedLibrary(
|
||||
identifier = s"${l.plainLibraryIdentifier}",
|
||||
classified = l.classified,
|
||||
tags = assignments.map(a => AssignedTag(name = tags(a.tagId).name, contextDependent = a.contextDependent)).sortBy(_.name.toLowerCase)
|
||||
)
|
||||
}
|
||||
Ok(Json.prettyPrint(Json.toJson(
|
||||
Export(libraryMapping = libraryMapping, tags = tags.values.toSeq.sortBy(_.name.toLowerCase))
|
||||
))).as(ContentTypes.JSON)
|
||||
}
|
||||
}
|
||||
|
||||
val tagsImportForm = Form(mapping("data" -> text)(identity)(Some(_)))
|
||||
|
||||
def tagsImport = AdminAction { implicit req =>
|
||||
Ok(views.html.tagsImport(tagsImportForm))
|
||||
}
|
||||
|
||||
def tagsImportAction = AdminAction.async { implicit req =>
|
||||
tagsImportForm.bindFromRequest()(req).fold(
|
||||
formWithErrors => ???,
|
||||
data =>
|
||||
export.exportFormats.reads(Json.parse(data)).fold(
|
||||
invalid => Future.successful(BadRequest(Txt("ERROR: "+invalid))),
|
||||
data => {
|
||||
def importTags() = tagsService.insertMany(data.tags)
|
||||
def getTagsByName(): Future[Map[String, Int]] = tagsService.all.map(_.groupBy(_._2.name).mapValues { case Seq((id, _)) => id }.map(identity))
|
||||
def importLibraries(): Future[Unit] = Future.sequence(
|
||||
data.libraryMapping.map{ taggedLibrary =>
|
||||
librariesService.insert(taggedLibrary.toLibrary).flatMap{ libraryId =>
|
||||
importLibraryTagAssignment(libraryId, taggedLibrary)
|
||||
}
|
||||
}
|
||||
).map( (x: Seq[Unit]) => ()) // I don't care about the result
|
||||
def importLibraryTagAssignment(libraryId: Int, taggedLibrary: export.TaggedLibrary): Future[Unit] = getTagsByName().flatMap { tagIdsByName =>
|
||||
Future.sequence(taggedLibrary.tags.map{ assignedTag =>
|
||||
val tagId = tagIdsByName(assignedTag.name)
|
||||
libraryTagAssignmentsService.insert(LibraryTagAssignment(LibraryTagPair(libraryId = libraryId, tagId = tagId), assignedTag.contextDependent)).map(_ => ())
|
||||
}).map( (x: Seq[Unit]) => ()) // I don't care about the result
|
||||
}
|
||||
for {
|
||||
_ <- importTags()
|
||||
_ <- importLibraries()
|
||||
} yield Ok(Txt("OK"))
|
||||
}
|
||||
)
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
def dependencies(requiredClassification: Option[Boolean], requiredTags: Seq[Int], noTag: Boolean) = ReadAction.async { implicit request =>
|
||||
val requiredTagsSet = requiredTags.toSet
|
||||
for{
|
||||
selectedDependencies <- db.run(librariesService.filtered(requiredClassification = requiredClassification, requiredTagsOption = if(noTag) None else Some(requiredTagsSet)).result)
|
||||
dependencyTags <- librariesService.byTags(selectedDependencies.map(_._1).toSet)
|
||||
allTags <- tagsService.all
|
||||
}yield{
|
||||
Ok(views.html.dependencies(
|
||||
requiredClassification = requiredClassification,
|
||||
selectedDependencies = selectedDependencies,
|
||||
allTags = allTags,
|
||||
dependencyTags = dependencyTags,
|
||||
requiredTagSet = requiredTagsSet,
|
||||
noTag = noTag,
|
||||
tagsLink = (newTags: Set[Int]) => routes.Application.dependencies(requiredClassification, newTags.toSeq.sorted, noTag),
|
||||
noTagLink = newNoTag => routes.Application.dependencies(requiredClassification, requiredTagsSet.toSeq.sorted, newNoTag),
|
||||
classificationLink = newClassification => routes.Application.dependencies(newClassification, requiredTagsSet.toSeq.sorted, noTag)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
def removeTag() = AdminAction.async(BodyParsers.parse.json) { request =>
|
||||
request.body.validate[LibraryTagPair].fold(
|
||||
err => Future.successful(BadRequest(Txt(err.toString()))),
|
||||
libraryTagPair => for(_ <- libraryTagAssignmentsService.remove(libraryTagPair)) yield Ok(Txt("OK"))
|
||||
)
|
||||
}
|
||||
|
||||
def addTag() = AdminAction.async(BodyParsers.parse.json) { request =>
|
||||
request.body.validate[LibraryTagAssignment].fold(
|
||||
err => Future.successful(BadRequest(Txt(err.toString()))),
|
||||
tagAssignment => for(_ <- libraryTagAssignmentsService.insert(tagAssignment)) yield {Ok(Txt("OK"))}
|
||||
)
|
||||
}
|
||||
|
||||
def setClassified(classified: Boolean) = AdminAction.async(BodyParsers.parse.json) {request =>
|
||||
val libraryId = request.body.as[Int]
|
||||
for(_ <- librariesService.setClassified(libraryId, classified)) yield Ok(Txt("OK"))
|
||||
}
|
||||
|
||||
def javascriptRoutes = Action { implicit request =>
|
||||
Ok(
|
||||
JavaScriptReverseRouter("Routes")(
|
||||
routes.javascript.Application.setClassified,
|
||||
routes.javascript.Application.addTag
|
||||
)
|
||||
).as("text/javascript")
|
||||
}
|
||||
|
||||
def testHttps(allowRedirect: Boolean) = Action { Ok(Txt(if(allowRedirect)
|
||||
"""
|
||||
|(function(){
|
||||
| var newUrl = window.location.href.replace(/^http:/, "https:");
|
||||
| if(newUrl != window.location.href){
|
||||
| window.location.replace(newUrl);
|
||||
| }
|
||||
|})();
|
||||
|""".stripMargin else "")).withHeaders("Content-type" -> "text/javascript; charset=utf-8") }
|
||||
|
||||
}
|
||||
64
app/controllers/AuthController.scala
Normal file
64
app/controllers/AuthController.scala
Normal file
@@ -0,0 +1,64 @@
|
||||
package controllers
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
import _root_.services.CredentialsVerificationService
|
||||
import com.mohiva.play.silhouette.api._
|
||||
import com.mohiva.play.silhouette.api.util.Clock
|
||||
import com.mohiva.play.silhouette.impl.authenticators.CookieAuthenticator
|
||||
import models.User
|
||||
import play.api.data.Form
|
||||
import play.api.data.Forms._
|
||||
import play.api.i18n.{Messages, MessagesApi}
|
||||
import play.api.libs.concurrent.Execution.Implicits._
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
final case class LoginRequest(username: String, password: String, rememberMe: Boolean)
|
||||
|
||||
class AuthController @Inject() (
|
||||
val messagesApi: MessagesApi,
|
||||
val env: Environment[User, CookieAuthenticator],
|
||||
clock: Clock,
|
||||
credentialsVerificationService: CredentialsVerificationService
|
||||
) extends AuthenticatedController {
|
||||
|
||||
val signInForm = Form(mapping(
|
||||
"username" -> nonEmptyText,
|
||||
"password" -> nonEmptyText,
|
||||
"rememberMe" -> boolean
|
||||
)(LoginRequest.apply)(LoginRequest.unapply))
|
||||
|
||||
def signIn = UserAwareAction { implicit request =>
|
||||
request.identity match {
|
||||
case Some(user) => Redirect(routes.Application.index(Map()))
|
||||
case None => Ok(views.html.auth.signIn(signInForm/*, socialProviderRegistry*/))
|
||||
}
|
||||
}
|
||||
|
||||
def authenticate() = UserAwareAction.async { implicit request =>
|
||||
signInForm.bindFromRequest().fold(
|
||||
formWithErrors => Future.successful(BadRequest(views.html.auth.signIn(formWithErrors/*, socialProviderRegistry*/))),
|
||||
loginRequest => {
|
||||
credentialsVerificationService.verifyCredentials(loginRequest.username, loginRequest.password).flatMap{
|
||||
case true =>
|
||||
val loginInfo: LoginInfo = LoginInfo(providerID = "credentials-verification", providerKey = loginRequest.username)
|
||||
val user: User = User(username = loginRequest.username)
|
||||
env.authenticatorService.create(loginInfo) flatMap { authenticator =>
|
||||
env.eventBus.publish(LoginEvent(user, request, implicitly[Messages]))
|
||||
env.authenticatorService.init(authenticator).flatMap(cookie =>
|
||||
env.authenticatorService.embed(cookie.copy(secure = request.secure), Redirect(routes.Application.index(Map())))
|
||||
)
|
||||
}
|
||||
case false => Future.successful(Redirect(routes.AuthController.signIn()).flashing("error" -> Messages("invalid.credentials")))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
def signOut = SecuredAction.async { implicit request =>
|
||||
val result = Redirect(routes.Application.index(Map()))
|
||||
env.eventBus.publish(LogoutEvent(request.identity, request, request2Messages))
|
||||
env.authenticatorService.discard(request.authenticator, result)
|
||||
}
|
||||
}
|
||||
31
app/controllers/AuthenticatedController.scala
Normal file
31
app/controllers/AuthenticatedController.scala
Normal file
@@ -0,0 +1,31 @@
|
||||
package controllers
|
||||
|
||||
import com.mohiva.play.silhouette.api.Silhouette
|
||||
import com.mohiva.play.silhouette.impl.authenticators.CookieAuthenticator
|
||||
import models.User
|
||||
import play.api.mvc.{Result, RequestHeader, Results}
|
||||
import views.html.DefaultRequest
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.language.implicitConversions
|
||||
|
||||
trait AuthenticatedControllerLowPriorityImplicits[T, C]{
|
||||
self: AuthenticatedController =>
|
||||
|
||||
protected object secureRequestConversion{
|
||||
implicit def securedRequestToUserAwareRequest(implicit req: SecuredRequest[_]): DefaultRequest = UserAwareRequest(Some(req.identity), authenticator = Some(req.authenticator), req.request)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class AuthenticatedController extends Silhouette[User, CookieAuthenticator] with AuthenticatedControllerLowPriorityImplicits[User, CookieAuthenticator]{
|
||||
|
||||
|
||||
override protected def onNotAuthenticated(request: RequestHeader): Option[Future[Result]] = Some(Future.successful(Redirect(routes.AuthController.signIn())))
|
||||
|
||||
object ReadAction extends SecuredActionBuilder with Results {
|
||||
|
||||
}
|
||||
|
||||
def AdminAction: SecuredActionBuilder = ???
|
||||
|
||||
}
|
||||
143
app/controllers/DependencyCheckReportsParser.scala
Normal file
143
app/controllers/DependencyCheckReportsParser.scala
Normal file
@@ -0,0 +1,143 @@
|
||||
package controllers
|
||||
|
||||
import java.net.URLEncoder
|
||||
|
||||
import com.google.inject.Inject
|
||||
import com.ysoft.odc._
|
||||
import controllers.DependencyCheckReportsParser.Result
|
||||
import models.PlainLibraryIdentifier
|
||||
import play.api.Logger
|
||||
import play.api.cache.CacheApi
|
||||
import play.twirl.api.Html
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
sealed trait Filter{
|
||||
def selector: Option[String]
|
||||
def subReports(r: Result): Option[Result]
|
||||
def filters: Boolean
|
||||
def descriptionHtml: Html
|
||||
def descriptionText: String
|
||||
}
|
||||
private final case class ProjectFilter(project: ReportInfo) extends Filter{
|
||||
override def filters: Boolean = true
|
||||
override def descriptionHtml: Html = views.html.filters.project(project)
|
||||
override def descriptionText: String = s"project ${friendlyProjectName(project)}"
|
||||
override def subReports(r: Result): Option[Result] = {
|
||||
@inline def reportInfo = project
|
||||
def f[T](m: Map[ReportInfo, T]): Map[String, T] = (
|
||||
if(reportInfo.subprojectNameOption.isEmpty) m.filter(_._1.projectId == project.projectId) else m.get(reportInfo).fold(Map.empty[ReportInfo, T])(x => Map(reportInfo -> x))
|
||||
).map{case (k, v) => k.fullId -> v}
|
||||
val newFlatReports = f(r.flatReports)
|
||||
val newFailedAnalysises = f(r.failedAnalysises)
|
||||
if(newFlatReports.isEmpty && newFailedAnalysises.isEmpty) None
|
||||
else Some(Result(bareFlatReports = newFlatReports, bareFailedAnalysises = newFailedAnalysises, projects = r.projects))
|
||||
}
|
||||
override def selector = Some(s"project:${project.fullId}")
|
||||
}
|
||||
private final case class TeamFilter(team: Team) extends Filter{
|
||||
override def filters: Boolean = true
|
||||
override def subReports(r: Result): Option[Result] = {
|
||||
|
||||
val reportInfoByFriendlyProjectName = r.projectsReportInfo.ungroupedReportsInfo.map(ri => friendlyProjectName(ri) -> ri).toSeq.groupBy(_._1).mapValues{
|
||||
case Seq((_, ri)) => ri
|
||||
case other => sys.error("some duplicate value: "+other)
|
||||
}.map(identity)
|
||||
val reportInfos = team.projectNames.map(reportInfoByFriendlyProjectName)
|
||||
def submap[T](m: Map[String, T]) = reportInfos.toSeq.flatMap(ri => m.get(ri.fullId).map(ri.fullId -> _) ).toMap
|
||||
Some(Result(
|
||||
bareFlatReports = submap(r.bareFlatReports),
|
||||
bareFailedAnalysises = submap(r.bareFailedAnalysises),
|
||||
projects = r.projects
|
||||
))
|
||||
}
|
||||
override def descriptionHtml: Html = views.html.filters.team(team.id)
|
||||
override def descriptionText: String = s"team ${team.name}"
|
||||
override def selector = Some(s"team:${team.id}")
|
||||
}
|
||||
object NoFilter extends Filter{
|
||||
override def filters: Boolean = false
|
||||
override val descriptionHtml: Html = views.html.filters.all()
|
||||
override def descriptionText: String = "all projects"
|
||||
override def subReports(r: Result): Option[Result] = Some(r)
|
||||
override def selector: Option[String] = None
|
||||
}
|
||||
private final case class BadFilter(pattern: String) extends Filter{
|
||||
override def filters: Boolean = true
|
||||
override def subReports(r: Result): Option[Result] = None
|
||||
override def descriptionHtml: Html = Html("<b>bad filter</b>")
|
||||
override def descriptionText: String = "bad filter"
|
||||
override def selector: Option[String] = Some(pattern)
|
||||
}
|
||||
|
||||
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)
|
||||
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))
|
||||
lazy val groupedDependencies = allDependencies.groupBy(_._1.hashes).values.map(GroupedDependency(_)).toSeq
|
||||
lazy val groupedDependenciesByPlainLibraryIdentifier: Map[PlainLibraryIdentifier, Set[GroupedDependency]] =
|
||||
groupedDependencies.toSet.flatMap((grDep: GroupedDependency) => grDep.plainLibraryIdentifiers.map(_ -> grDep)).groupBy(_._1).mapValues(_.map(_._2)).map(identity)
|
||||
lazy val vulnerableDependencies = groupedDependencies.filter(_.vulnerabilities.nonEmpty)
|
||||
|
||||
private val ProjectSelectorPattern = """^project:(.*)$""".r
|
||||
private val TeamSelectorPattern = """^team:(.*)$""".r
|
||||
|
||||
private def parseFilter(filter: String): Filter = filter match {
|
||||
case ProjectSelectorPattern(project) => ProjectFilter(projectsReportInfo.reportIdToReportInfo(project))
|
||||
case TeamSelectorPattern(team) => TeamFilter(projects.teamById(team))
|
||||
case other => BadFilter(other)
|
||||
}
|
||||
|
||||
def selection(selectorOption: Option[String]): Option[ResultWithSelection] = {
|
||||
val filter = selectorOption.map(parseFilter).getOrElse(NoFilter)
|
||||
filter.subReports(this).map{ result =>
|
||||
ResultWithSelection(
|
||||
result = result,
|
||||
projectsWithSelection = ProjectsWithSelection(filter = filter, projectsWithReports = projectsReportInfo, teams = projects.teamSet)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
final class DependencyCheckReportsParser @Inject() (cache: CacheApi, projects: Projects) {
|
||||
|
||||
def parseReports(successfulResults: Map[String, (Build, ArtifactItem, ArtifactFile)]) = {
|
||||
val rid = math.random.toString // for logging
|
||||
@volatile var parseFailedForSomeAnalysis = false
|
||||
val deepReportsTriesIterable: Iterable[Map[String, Try[Analysis]]] = for((k, (build, data, log)) <- successfulResults) yield {
|
||||
Logger.debug(data.flatFilesWithPrefix(s"$k/").keySet.toSeq.sorted.toString)
|
||||
val flat = data.flatFilesWithPrefix(s"$k/")
|
||||
(for((k, v) <- flat.par) yield {
|
||||
val analysisKey = URLEncoder.encode(s"analysis/parsedXml/${build.buildResultKey}/${k}", "utf-8")
|
||||
Logger.debug(s"[$rid] analysisKey: $analysisKey")
|
||||
val analysisTry = cache.getOrElse(analysisKey)(Try{OdcParser.parseXmlReport(v)})
|
||||
analysisTry match{
|
||||
case Success(e) => // nothing
|
||||
case Failure(e) =>
|
||||
if(!parseFailedForSomeAnalysis){
|
||||
Logger.error(s"[$rid] Cannot parse $k: ${new String(v, "utf-8")}", e)
|
||||
parseFailedForSomeAnalysis = true
|
||||
}
|
||||
}
|
||||
k -> analysisTry
|
||||
}).seq
|
||||
}
|
||||
val deepReportsAndFailuresIterable = deepReportsTriesIterable.map { reports =>
|
||||
val (successfulReportsTries, failedReportsTries) = reports.partition(_._2.isSuccess)
|
||||
val successfulReports = successfulReportsTries.mapValues(_.asInstanceOf[Success[Analysis]].value).map(identity)
|
||||
val failedReports = failedReportsTries.mapValues(_.asInstanceOf[Failure[Analysis]].exception).map(identity)
|
||||
(successfulReports, failedReports)
|
||||
}
|
||||
val deepSuccessfulReports = deepReportsAndFailuresIterable.map(_._1).toSeq
|
||||
val failedAnalysises = deepReportsAndFailuresIterable.map(_._2).toSeq.flatten.toMap
|
||||
val flatReports = deepSuccessfulReports.flatten.toMap
|
||||
Logger.debug(s"[$rid] parse finished")
|
||||
Result(flatReports, failedAnalysises, projects)
|
||||
}
|
||||
|
||||
}
|
||||
106
app/controllers/DependencyCheckReportsProcessor.scala
Normal file
106
app/controllers/DependencyCheckReportsProcessor.scala
Normal file
@@ -0,0 +1,106 @@
|
||||
package controllers
|
||||
|
||||
import com.github.nscala_time.time.Imports._
|
||||
import com.google.inject.Inject
|
||||
import com.google.inject.name.Named
|
||||
import com.ysoft.odc.Checks._
|
||||
import com.ysoft.odc._
|
||||
import org.joda.time.DateTimeConstants
|
||||
import play.api.Logger
|
||||
import play.api.i18n.{I18nSupport, MessagesApi}
|
||||
import play.api.mvc.RequestHeader
|
||||
import play.twirl.api.Html
|
||||
import views.html.DefaultRequest
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
final case class MissingGavExclusions(exclusionsSet: Set[Exclusion]){
|
||||
def isExcluded(groupedDependency: GroupedDependency) = exclusionsSet.exists(_.matches(groupedDependency))
|
||||
}
|
||||
|
||||
final class DependencyCheckReportsProcessor @Inject() (
|
||||
@Named("bamboo-server-url") val server: String,
|
||||
dependencyCheckReportsParser: DependencyCheckReportsParser,
|
||||
@Named("missing-GAV-exclusions") missingGAVExclusions: MissingGavExclusions,
|
||||
val messagesApi: MessagesApi
|
||||
) extends I18nSupport {
|
||||
|
||||
private def parseDateTime(dt: String): DateTime = {
|
||||
if(dt.forall(_.isDigit)){
|
||||
new DateTime(dt.toLong) // TODO: timezone (I don't care much, though)
|
||||
}else{
|
||||
val formatter = DateTimeFormat.forPattern("dd/MM/yyyy HH:mm:ss") // TODO: timezone (I don't care much, though)
|
||||
formatter.parseDateTime(dt)
|
||||
}
|
||||
}
|
||||
|
||||
@deprecated("use HTML output instead", "SNAPSHOT") private val showDependencies: (Seq[GroupedDependency]) => Seq[String] = {
|
||||
_.map { s =>
|
||||
s.dependencies.map { case (dep, projects) => s"${dep.fileName} @ ${projects.toSeq.sorted.map(friendlyProjectName).mkString(", ")}" }.mkString(", ") + " " + s.hashes
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def processResults(
|
||||
resultsFuture: Future[(Map[String, (Build, ArtifactItem, ArtifactFile)], Map[String, Throwable])],
|
||||
requiredVersions: Map[String, Int]
|
||||
)(implicit requestHeader: DefaultRequest, snoozesInfo: SnoozesInfo, executionContext: ExecutionContext) = try{
|
||||
for((successfulResults, failedResults) <- resultsFuture) yield{
|
||||
val reportResult = dependencyCheckReportsParser.parseReports(successfulResults)
|
||||
import reportResult.{allDependencies, failedAnalysises, flatReports, groupedDependencies, vulnerableDependencies}
|
||||
val now = DateTime.now
|
||||
val oldReportThreshold = now - 1.day
|
||||
val cveTimestampThreshold = now - (if(now.dayOfWeek().get == DateTimeConstants.MONDAY) 4.days else 2.days )
|
||||
val ScanChecks: Seq[Map[ReportInfo, Analysis] => Option[Warning]] = Seq(
|
||||
differentValues("scan infos", "scan-info", WarningSeverity.Warning)(_.groupBy(_._2.scanInfo).mapValues(_.keys.toIndexedSeq.sorted)),
|
||||
badValues("old-reports", "old reports", WarningSeverity.Warning)((_, a) => if(a.reportDate < oldReportThreshold) Some(Html(a.reportDate.toString)) else None),
|
||||
badValues("bad-cve-data", "old or no CVE data", WarningSeverity.Warning){(_, analysis) =>
|
||||
(analysis.scanInfo.xml \\ "timestamp").map(_.text).filterNot(_ == "").map(parseDateTime) match {
|
||||
case Seq() => Some(Html("no data"))
|
||||
case timestamps =>
|
||||
val newestTimestamp = timestamps.max
|
||||
val oldestTimestamp = timestamps.min
|
||||
if(newestTimestamp < cveTimestampThreshold) Some(Html(newestTimestamp.toString))
|
||||
else None
|
||||
}
|
||||
}
|
||||
)
|
||||
val GroupedDependenciesChecks = Seq[Seq[GroupedDependency] => Option[Warning]](
|
||||
badGroupedDependencies("unidentified-dependencies", "unidentified dependencies", WarningSeverity.Info)(_.filter(_.dependencies.exists(_._1.identifiers.isEmpty)))(show = showDependencies, exclusions = missingGAVExclusions.exclusionsSet),
|
||||
badGroupedDependencies("different-identifier-sets", "different identifier sets", WarningSeverity.Info)(_.filter(_.dependencies.groupBy(_._1.identifiers).size > 1).toIndexedSeq)(),
|
||||
badGroupedDependencies("different-evidence", "different evidence", WarningSeverity.Info)(_.filter(_.dependencies.groupBy(_._1.evidenceCollected).size > 1).toIndexedSeq)(show = x => Some(views.html.warnings.groupedDependencies(x))),
|
||||
badGroupedDependencies("missing-gav", "missing GAV", WarningSeverity.Info)(_.filter(_.identifiers.filter(_.identifierType == "maven").isEmpty))(show = showDependencies, exclusions = missingGAVExclusions.exclusionsSet)
|
||||
)
|
||||
|
||||
val unknownIdentifierTypes = allDependencies.flatMap(_._1.identifiers.map(_.identifierType)).toSet -- Set("maven", "cpe")
|
||||
val failedReports = successfulResults.filter(x => x._2._1.state != "Successful" || x._2._1.buildState != "Successful")
|
||||
val extraWarnings = Seq[Option[Warning]](
|
||||
if(failedReports.size > 0) Some(IdentifiedWarning("failed-reports", views.html.warnings.failedReports(failedReports.values.map{case (b, _ ,_) => b}.toSet, server), WarningSeverity.Error)) else None,
|
||||
if(unknownIdentifierTypes.size > 0) Some(IdentifiedWarning("unknown-identifier-types", views.html.warnings.unknownIdentifierType(unknownIdentifierTypes), WarningSeverity.Info)) else None,
|
||||
{
|
||||
val emptyResults = successfulResults.filter{case (k, (_, dir, _)) => dir.flatFiles.size < 1}
|
||||
if(emptyResults.nonEmpty) Some(IdentifiedWarning("empty-results", views.html.warnings.emptyResults(emptyResults.values.map{case (build, _, _) => build}.toSeq, server), WarningSeverity.Warning)) else None
|
||||
},
|
||||
{
|
||||
val resultsWithErrorMessages = successfulResults.filter{case (k, (_, _, log)) => log.dataString.lines.exists(l => (l.toLowerCase startsWith "error") || (l.toLowerCase contains "[error]"))}
|
||||
if(resultsWithErrorMessages.nonEmpty) Some(IdentifiedWarning("results-with-error-messages", views.html.warnings.resultsWithErrorMessages(resultsWithErrorMessages.values.map{case (build, _, _) => build}.toSeq, server), WarningSeverity.Error)) else None
|
||||
},
|
||||
if(failedResults.isEmpty) None else Some(IdentifiedWarning("failed-results", views.html.warnings.failedResults(failedResults), WarningSeverity.Error)),
|
||||
if(requiredVersions.isEmpty) None else Some(IdentifiedWarning("required-versions", views.html.warnings.textWarning("You have manually requested results for some older version."), WarningSeverity.Warning)),
|
||||
if(failedAnalysises.isEmpty) None else Some(IdentifiedWarning("failed-analysises", views.html.warnings.textWarning(s"Some reports failed to parse: ${failedAnalysises.keySet}"), WarningSeverity.Error))
|
||||
).flatten
|
||||
|
||||
val scanWarnings = ScanChecks.flatMap(_(flatReports))
|
||||
val groupedDependenciesWarnings = GroupedDependenciesChecks.flatMap(_(groupedDependencies))
|
||||
val allWarnings = scanWarnings ++ groupedDependenciesWarnings ++ extraWarnings
|
||||
|
||||
// TODO: log analysis
|
||||
// TODO: related dependencies
|
||||
(vulnerableDependencies, allWarnings, groupedDependencies)
|
||||
}
|
||||
}finally{
|
||||
Logger.debug("Reports processed")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
40
app/controllers/ProjectReportsProvider.scala
Normal file
40
app/controllers/ProjectReportsProvider.scala
Normal file
@@ -0,0 +1,40 @@
|
||||
package controllers
|
||||
|
||||
import com.github.nscala_time.time.Imports._
|
||||
import com.google.inject.Inject
|
||||
import com.ysoft.odc.Downloader
|
||||
import play.api.cache.CacheApi
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.reflect.ClassTag
|
||||
import scala.util.Success
|
||||
|
||||
class ProjectReportsProvider @Inject() (
|
||||
bambooDownloader: Downloader,
|
||||
cache: CacheApi,
|
||||
projects: Projects
|
||||
)(implicit executionContext: ExecutionContext){
|
||||
|
||||
private def bambooCacheKey(versions: Map[String, Int]) = "bamboo/results/" + versions.toSeq.sorted.map{case (k, v) => k.getBytes("utf-8").mkString("-") + ":" + v}.mkString("|")
|
||||
|
||||
def purgeCache(versions: Map[String, Int]) = cache.remove(bambooCacheKey(versions))
|
||||
|
||||
private def getOrElseFuture[T: ClassTag]
|
||||
(name: String, expiration: scala.concurrent.duration.Duration = scala.concurrent.duration.Duration.Inf)
|
||||
(f: => Future[T])
|
||||
(implicit executionContext: ExecutionContext): Future[T] =
|
||||
{
|
||||
cache.get[T](name).map(Future.successful).getOrElse(
|
||||
f.andThen{
|
||||
case Success(value) =>cache.set(name, value, expiration)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
def resultsForVersions(versions: Map[String, Int]) = {
|
||||
def get = {val time = DateTime.now; bambooDownloader.downloadProjectReports(projects.projectSet, versions).map(time -> _)}
|
||||
val allFuture = getOrElseFuture(bambooCacheKey(versions)){println("CACHE MISS"); get}
|
||||
(allFuture.map(_._1), allFuture.map(_._2))
|
||||
}
|
||||
|
||||
}
|
||||
52
app/controllers/Projects.scala
Normal file
52
app/controllers/Projects.scala
Normal file
@@ -0,0 +1,52 @@
|
||||
package controllers
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
import play.api.Configuration
|
||||
|
||||
class Projects @Inject() (configuration: Configuration) {
|
||||
import scala.collection.JavaConversions._
|
||||
val projectMap = {
|
||||
val projectsConfig = configuration.getObject("yssdc.projects").getOrElse(sys.error("yssdc.projects is not set")).toConfig
|
||||
projectsConfig.entrySet().map( k => k.getKey -> projectsConfig.getString(k.getKey)).toMap
|
||||
}
|
||||
val projectSet = projectMap.keySet
|
||||
val teamIdSet = configuration.getStringSeq("yssdc.teams").getOrElse(sys.error("yssdc.teams is not set")).map(TeamId).toSet
|
||||
private val teamsByIds = teamIdSet.map(t => t.id -> t).toMap
|
||||
val teamLeaders = {
|
||||
import scala.collection.JavaConversions._
|
||||
configuration.getObject("yssdc.teamLeaders").getOrElse(sys.error("yssdc.teamLeaders is not set")).map{case(k, v) =>
|
||||
TeamId(k) -> v.unwrapped().asInstanceOf[String]
|
||||
}
|
||||
}
|
||||
{
|
||||
val extraTeams = teamLeaders.keySet -- teamIdSet
|
||||
if(extraTeams.nonEmpty){
|
||||
sys.error(s"Some unexpected teams: $extraTeams")
|
||||
}
|
||||
}
|
||||
|
||||
def existingTeamId(s: String): TeamId = teamsByIds(s)
|
||||
|
||||
val projectToTeams = configuration.getObject("yssdc.projectsToTeams").get.mapValues{_.unwrapped().asInstanceOf[java.util.List[String]].map(c =>
|
||||
existingTeamId(c)
|
||||
).toSet}.map(identity)
|
||||
|
||||
val projectAndTeams = projectToTeams.toSeq.flatMap{case (project, teams) => teams.map(team => (project, team))}
|
||||
|
||||
val teamsToProjects = projectAndTeams.groupBy(_._2).mapValues(_.map(_._1).toSet).map(identity)
|
||||
|
||||
val teamsById: Map[String, Team] = for{
|
||||
(team, projectNames) <- teamsToProjects
|
||||
} yield team.id -> Team(
|
||||
id = team.id,
|
||||
name = team.name,
|
||||
leader = teamLeaders(team),
|
||||
projectNames = projectNames
|
||||
)
|
||||
|
||||
def teamById(id: String) = teamsById(id)
|
||||
|
||||
def teamSet = teamsById.values.toSet
|
||||
|
||||
}
|
||||
58
app/controllers/ProjectsWithReports.scala
Normal file
58
app/controllers/ProjectsWithReports.scala
Normal file
@@ -0,0 +1,58 @@
|
||||
package controllers
|
||||
|
||||
final case class ReportInfo(
|
||||
projectId: String,
|
||||
projectName: String,
|
||||
fullId: String,
|
||||
subprojectNameOption: Option[String]
|
||||
) extends Ordered[ReportInfo] {
|
||||
|
||||
import scala.math.Ordered.orderingToOrdered
|
||||
|
||||
override def compare(that: ReportInfo): Int = ((projectName, subprojectNameOption, fullId)) compare ((that.projectName, that.subprojectNameOption, that.fullId))
|
||||
|
||||
// It seems to be a good idea to have a custom equals and hashCode for performance reasons
|
||||
|
||||
|
||||
override def equals(other: Any): Boolean = other match {
|
||||
case other: ReportInfo => fullId == other.fullId
|
||||
case _ => false
|
||||
}
|
||||
|
||||
override def hashCode(): Int = 517+fullId.hashCode
|
||||
|
||||
}
|
||||
|
||||
object ProjectsWithReports{
|
||||
|
||||
private val RestMessBeginRegexp = """^/Report results-XML/""".r
|
||||
|
||||
private val RestMessEndRegexp = """/(target/)?dependency-check-report\.xml$""".r
|
||||
|
||||
}
|
||||
|
||||
class ProjectsWithReports (val projects: Projects, val reports: Set[String]) {
|
||||
|
||||
import ProjectsWithReports._
|
||||
|
||||
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(_ != "")
|
||||
subProjectOption.fold(baseName)(baseName+"/"+_)
|
||||
unfriendlyName -> ReportInfo(
|
||||
projectId = baseName,
|
||||
fullId = unfriendlyName,
|
||||
projectName = projects.projectMap(baseName),
|
||||
subprojectNameOption = subProjectOption
|
||||
)
|
||||
}.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
|
||||
|
||||
}
|
||||
14
app/controllers/ProjectsWithSelection.scala
Normal file
14
app/controllers/ProjectsWithSelection.scala
Normal file
@@ -0,0 +1,14 @@
|
||||
package controllers
|
||||
|
||||
final case class TeamId(id: String) extends AnyVal {
|
||||
def name = id
|
||||
}
|
||||
|
||||
final case class Team(id: String, name: String, leader: String, projectNames: Set[String])
|
||||
|
||||
// TODO: rename to something more sane. It is maybe rather FilteringData now.
|
||||
final case class ProjectsWithSelection(filter: Filter, projectsWithReports: ProjectsWithReports, teams: Set[Team]) {
|
||||
def isProjectSpecified: Boolean = filter.filters
|
||||
def selectorString = filter.selector
|
||||
def projectNameText: String = filter.descriptionText
|
||||
}
|
||||
226
app/controllers/Statistics.scala
Normal file
226
app/controllers/Statistics.scala
Normal file
@@ -0,0 +1,226 @@
|
||||
package controllers
|
||||
|
||||
import com.github.nscala_time.time.Imports._
|
||||
import com.google.inject.Inject
|
||||
import com.google.inject.name.Named
|
||||
import com.ysoft.odc.{ArtifactFile, ArtifactItem}
|
||||
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 views.html.DefaultRequest
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
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 vulnerabilitiesToDependencies: Map[Vulnerability, Set[GroupedDependency]] = vulnerableDependencies.flatMap(dep =>
|
||||
dep.vulnerabilities.map(vuln => (vuln, dep))
|
||||
).groupBy(_._1).mapValues(_.map(_._2)).map(identity)
|
||||
vulnerableDependencies.flatMap(dep => dep.vulnerabilities.map(_ -> dep)).groupBy(_._1).mapValues(_.map(_._2)).map(identity)
|
||||
vulnerableDependencies.flatMap(dep => dep.vulnerabilities.map(_ -> dep)).groupBy(_._1).mapValues(_.map(_._2)).map(identity)
|
||||
lazy val vulnerableDependencies = dependencies.filter(_.isVulnerable)
|
||||
lazy val (dependenciesWithCpe, dependenciesWithoutCpe) = dependencies.partition(_.hasCpe)
|
||||
lazy val cpeRatio = dependenciesWithCpe.size.toDouble / dependencies.size.toDouble
|
||||
lazy val weaknesses = vulnerabilities.flatMap(_.cweOption)
|
||||
lazy val weaknessesFrequency = computeWeaknessesFrequency(vulnerabilities)
|
||||
}
|
||||
case class TagStatistics(tagRecord: (Int, LibraryTag), stats: LibDepStatistics){
|
||||
def tag: LibraryTag = tagRecord._2
|
||||
def tagId: Int = tagRecord._1
|
||||
}
|
||||
|
||||
def computeWeaknessesFrequency(vulnerabilities: Set[Vulnerability]) = vulnerabilities.toSeq.map(_.cweOption).groupBy(identity).mapValues(_.size).map(identity).withDefaultValue(0)
|
||||
|
||||
}
|
||||
|
||||
import controllers.Statistics._
|
||||
|
||||
class Statistics @Inject() (
|
||||
reportsParser: DependencyCheckReportsParser,
|
||||
reportsProcessor: DependencyCheckReportsProcessor,
|
||||
projectReportsProvider: ProjectReportsProvider,
|
||||
dependencyCheckReportsParser: DependencyCheckReportsParser,
|
||||
librariesService: LibrariesService,
|
||||
tagsService: TagsService,
|
||||
odcService: OdcService,
|
||||
libraryTagAssignmentsService: LibraryTagAssignmentsService,
|
||||
@Named("missing-GAV-exclusions") missingGAVExclusions: MissingGavExclusions,
|
||||
projects: Projects,
|
||||
val env: AuthEnv
|
||||
)(implicit val messagesApi: MessagesApi, executionContext: ExecutionContext) extends AuthenticatedController {
|
||||
|
||||
private val versions = Map[String, Int]()
|
||||
|
||||
private def notFound()(implicit req: DefaultRequest) = {
|
||||
NotFound(views.html.defaultpages.notFound("GET", req.uri))
|
||||
}
|
||||
|
||||
import secureRequestConversion._
|
||||
|
||||
|
||||
private def select(successfulResults: Map[String, (Build, ArtifactItem, ArtifactFile)], selectorOption: Option[String]) = dependencyCheckReportsParser.parseReports(successfulResults).selection(selectorOption)
|
||||
|
||||
def searchVulnerableSoftware(versionlessCpes: Seq[String], versionOption: Option[String]) = ReadAction.async{ implicit req =>
|
||||
if(versionlessCpes.isEmpty){
|
||||
Future.successful(notFound())
|
||||
}else{
|
||||
val now = DateTime.now()
|
||||
val oldDataThreshold = 2.days
|
||||
val lastDbUpdateFuture = odcService.loadLastDbUpdate()
|
||||
val isOldFuture = lastDbUpdateFuture.map{ lastUpdate => now - oldDataThreshold > lastUpdate}
|
||||
versionOption match {
|
||||
case Some(version) =>
|
||||
for {
|
||||
res1 <- Future.traverse(versionlessCpes) { versionlessCpe => odcService.findRelevantCpes(versionlessCpe, version) }
|
||||
vulnIds = res1.flatten.map(_.vulnerabilityId).toSet
|
||||
vulns <- Future.traverse(vulnIds)(id => odcService.getVulnerabilityDetails(id).map(_.get))
|
||||
isOld <- isOldFuture
|
||||
} yield Ok(views.html.statistics.vulnerabilitiesForLibrary(
|
||||
vulnsAndVersionOption = Some((vulns, version)),
|
||||
cpes = versionlessCpes,
|
||||
isDbOld = isOld
|
||||
))
|
||||
case None =>
|
||||
for(isOld <- isOldFuture) yield Ok(views.html.statistics.vulnerabilitiesForLibrary(
|
||||
vulnsAndVersionOption = None,
|
||||
cpes = versionlessCpes,
|
||||
isDbOld = isOld
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def basic(projectOption: Option[String]) = ReadAction.async{ implicit req =>
|
||||
val (lastRefreshTime, resultsFuture) = projectReportsProvider.resultsForVersions(versions)
|
||||
resultsFuture flatMap { case (successfulResults, failedResults) =>
|
||||
select(successfulResults, projectOption).fold(Future.successful(notFound())){ selection =>
|
||||
val tagsFuture = tagsService.all
|
||||
val parsedReports = selection.result
|
||||
for{
|
||||
tagStatistics <- statisticsForTags(parsedReports, tagsFuture)
|
||||
} yield Ok(views.html.statistics.basic(
|
||||
tagStatistics = tagStatistics,
|
||||
projectsWithSelection = selection.projectsWithSelection,
|
||||
parsedReports = parsedReports
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def statisticsForTags(parsedReports: DependencyCheckReportsParser.Result, tagsFuture: Future[Seq[(Int, LibraryTag)]]): Future[Seq[Statistics.TagStatistics]] = {
|
||||
val librariesFuture = librariesService.byPlainLibraryIdentifiers(parsedReports.allDependencies.flatMap(_._1.plainLibraryIdentifiers).toSet)
|
||||
val libraryTagAssignmentsFuture = librariesFuture.flatMap{libraries => libraryTagAssignmentsService.forLibraries(libraries.values.map(_._1).toSet)}
|
||||
val tagsToLibrariesFuture = libraryTagAssignmentsService.tagsToLibraries(libraryTagAssignmentsFuture)
|
||||
val librariesToDependencies = parsedReports.groupedDependenciesByPlainLibraryIdentifier
|
||||
for{
|
||||
librariesById <- librariesFuture.map(_.values.toMap)
|
||||
tagsToLibraries <- tagsToLibrariesFuture
|
||||
tags <- tagsFuture
|
||||
} yield tags.flatMap{case tagRecord @ (tagId, tag) =>
|
||||
val libraryAssignments = tagsToLibraries(tagId)
|
||||
val tagLibraries = libraryAssignments.map(a => a.libraryId -> librariesById(a.libraryId))
|
||||
val tagDependencies: Set[GroupedDependency] = tagLibraries.flatMap{case (_, lib) => librariesToDependencies(lib.plainLibraryIdentifier)}
|
||||
// TODO: vulnerabilities in the past
|
||||
if(tagLibraries.isEmpty) None
|
||||
else Some(TagStatistics(tagRecord = tagRecord, stats = LibDepStatistics(libraries = tagLibraries, dependencies = tagDependencies)))
|
||||
}
|
||||
}
|
||||
|
||||
def vulnerabilities(projectOption: Option[String], tagIdOption: Option[Int]) = ReadAction.async {implicit req =>
|
||||
val (lastRefreshTime, resultsFuture) = projectReportsProvider.resultsForVersions(versions)
|
||||
resultsFuture flatMap { case (successfulResults, failedResults) =>
|
||||
select(successfulResults, projectOption).fold(Future.successful(notFound())){ selection =>
|
||||
val parsedReports = selection.result
|
||||
for{
|
||||
libraries <- librariesService.byPlainLibraryIdentifiers(parsedReports.allDependencies.flatMap(_._1.plainLibraryIdentifiers).toSet)
|
||||
tagOption <- tagIdOption.fold[Future[Option[(Int, LibraryTag)]]](Future.successful(None))(tagId => tagsService.getById(tagId).map(Some(_)))
|
||||
statistics <- tagOption.fold(Future.successful(LibDepStatistics(dependencies = parsedReports.groupedDependencies.toSet, libraries = libraries.values.toSet))){ tag =>
|
||||
statisticsForTags(parsedReports, Future.successful(Seq(tag))).map{
|
||||
case Seq(TagStatistics(_, stats)) => stats // statisticsForTags is designed for multiple tags, but we have just one…
|
||||
case Seq() => LibDepStatistics(libraries = Set(), dependencies = Set()) // We don't want to crash when no dependencies are there…
|
||||
}
|
||||
}
|
||||
} yield Ok(views.html.statistics.vulnerabilities(
|
||||
projectsWithSelection = selection.projectsWithSelection,
|
||||
tagOption = tagOption,
|
||||
statistics = statistics
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def vulnerability(name: String, projectOption: Option[String]) = ReadAction.async { implicit req =>
|
||||
val (lastRefreshTime, resultsFuture) = projectReportsProvider.resultsForVersions(versions)
|
||||
resultsFuture flatMap { case (successfulResults, failedResults) =>
|
||||
select(successfulResults, projectOption).fold(Future.successful(notFound())){ selection =>
|
||||
val relevantReports = selection.result
|
||||
val vulns = relevantReports.vulnerableDependencies.flatMap(dep => dep.vulnerabilities.map(vuln => (vuln, dep))).groupBy(_._1.name).mapValues{case vulnsWithDeps =>
|
||||
val (vulnSeq, depSeq) = vulnsWithDeps.unzip
|
||||
val Seq(vuln) = vulnSeq.toSet.toSeq // Will fail when there are more different descriptions for one vulnerability…
|
||||
vuln -> depSeq.toSet
|
||||
}// .map(identity) // The .map(identity) materializes lazily mapped Map (because .mapValues is lazy). I am, however, unsure if this is a good idea. Probably not.
|
||||
vulns.get(name).fold(Future.successful(Ok(views.html.statistics.vulnerabilityNotFound(
|
||||
name = name,
|
||||
projectsWithSelection = selection.projectsWithSelection
|
||||
)))){ case (vuln, vulnerableDependencies) =>
|
||||
for(
|
||||
plainLibs <- librariesService.byPlainLibraryIdentifiers(vulnerableDependencies.flatMap(_.plainLibraryIdentifiers)).map(_.keySet)
|
||||
) 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
|
||||
))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def vulnerableLibraries(project: Option[String]) = ReadAction.async { implicit req =>
|
||||
val (lastRefreshTime, resultsFuture) = projectReportsProvider.resultsForVersions(versions)
|
||||
resultsFuture flatMap { case (successfulResults, failedResults) =>
|
||||
select(successfulResults, project).fold(Future.successful(notFound())){selection =>
|
||||
val reports = selection.result
|
||||
Future.successful(Ok(views.html.statistics.vulnerableLibraries(
|
||||
projectsWithSelection = selection.projectsWithSelection,
|
||||
vulnerableDependencies = reports.vulnerableDependencies,
|
||||
allDependenciesCount = reports.groupedDependencies.size
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def allLibraries(project: Option[String]) = ReadAction.async { implicit req =>
|
||||
val (lastRefreshTime, resultsFuture) = projectReportsProvider.resultsForVersions(versions)
|
||||
resultsFuture flatMap { case (successfulResults, failedResults) =>
|
||||
select(successfulResults, project).fold(Future.successful(notFound())){selection =>
|
||||
Future.successful(Ok(views.html.statistics.allLibraries(
|
||||
projectsWithSelection = selection.projectsWithSelection,
|
||||
allDependencies = selection.result.groupedDependencies
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def allGavs(project: Option[String]) = ReadAction.async { implicit req =>
|
||||
val (lastRefreshTime, resultsFuture) = projectReportsProvider.resultsForVersions(versions)
|
||||
resultsFuture flatMap { case (successfulResults, failedResults) =>
|
||||
select(successfulResults, project).fold(Future.successful(notFound())){selection =>
|
||||
Future.successful(Ok(Txt(
|
||||
selection.result.groupedDependencies.flatMap(_.mavenIdentifiers).toSet.toIndexedSeq.sortBy((id: Identifier) => (id.identifierType, id.name)).map(id => id.name.split(':') match {
|
||||
case Array(g, a, v) =>
|
||||
s""""${id.identifierType}", "$g", "$a", "$v", "${id.url}" """
|
||||
}).mkString("\n")
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
37
app/controllers/package.scala
Normal file
37
app/controllers/package.scala
Normal file
@@ -0,0 +1,37 @@
|
||||
import com.mohiva.play.silhouette.api.Environment
|
||||
import com.mohiva.play.silhouette.impl.authenticators.CookieAuthenticator
|
||||
import models.{User, SnoozeInfo}
|
||||
|
||||
/**
|
||||
* Created by user on 7/15/15.
|
||||
*/
|
||||
package object controllers {
|
||||
|
||||
// Imports for all templates. Those could be added directly to the template files, but IntelliJ IDEA does not like it.
|
||||
type Dependency = com.ysoft.odc.Dependency
|
||||
type Build = com.ysoft.odc.Build
|
||||
type GroupedDependency = com.ysoft.odc.GroupedDependency
|
||||
type Vulnerability = com.ysoft.odc.Vulnerability
|
||||
type Identifier = com.ysoft.odc.Identifier
|
||||
type DateTime = org.joda.time.DateTime
|
||||
type SnoozesInfo = Map[String, SnoozeInfo]
|
||||
type AuthEnv = Environment[User, CookieAuthenticator]
|
||||
|
||||
|
||||
val NormalUrlPattern = """^(http(s?)|ftp(s?))://.*""".r
|
||||
|
||||
val TooGenericDomains = Set("sourceforge.net", "github.com", "github.io")
|
||||
|
||||
|
||||
/* def friendlyProjectName(unfriendlyName: String) = {
|
||||
val (baseName, theRest) = unfriendlyName.span(_ != '/')
|
||||
//theRest.drop(1)
|
||||
val removeLeadingMess = RestMessBeginRegexp.replaceAllIn(_: String, "")
|
||||
val removeTrailingMess = RestMessEndRegexp.replaceAllIn(_: String, "")
|
||||
val removeMess = removeLeadingMess andThen removeTrailingMess
|
||||
val subProjectOption = Some(removeMess(theRest)).filter(_ != "")
|
||||
subProjectOption.fold(baseName)(baseName+"/"+_)
|
||||
}*/
|
||||
def friendlyProjectName(reportInfo: ReportInfo) = reportInfo.subprojectNameOption.fold(reportInfo.projectName)(reportInfo.projectName+": "+_)
|
||||
|
||||
}
|
||||
21
app/controllers/warnings.scala
Normal file
21
app/controllers/warnings.scala
Normal file
@@ -0,0 +1,21 @@
|
||||
package controllers
|
||||
|
||||
import controllers.WarningSeverity.WarningSeverity
|
||||
import play.twirl.api.Html
|
||||
|
||||
object WarningSeverity extends Enumeration {
|
||||
type WarningSeverity = Value
|
||||
// Order is important
|
||||
val Info = Value("info")
|
||||
val Warning = Value("warning")
|
||||
val Error = Value("error")
|
||||
}
|
||||
|
||||
sealed abstract class Warning {
|
||||
def html: Html
|
||||
def id: String
|
||||
def allowSnoozes = true
|
||||
def severity: WarningSeverity
|
||||
}
|
||||
|
||||
final case class IdentifiedWarning(id: String, html: Html, severity: WarningSeverity) extends Warning
|
||||
Reference in New Issue
Block a user