Added new ODC scans for Java libraries. Those can scan even transitive dependencies and can be run before adding a new library to a project.

This commit is contained in:
Šesták Vít
2017-07-31 12:09:23 +02:00
parent bb0089cd97
commit 2049759430
31 changed files with 824 additions and 200 deletions

View File

@@ -184,7 +184,7 @@ h3.library-identification{
content: "";
display: none;
}
.dependencies-table .severity .tooltip-inner {
.dependencies-table .severity .tooltip-inner, #library-identifier-wrapper .tooltip-inner {
white-space:nowrap;
max-width:none;
}
@@ -242,4 +242,11 @@ h3.library-identification{
.sublist{
padding-left: 23px;
}
#library-identifier-wrapper .tooltip-inner{
text-align: left;
}
#scan-results{
margin-top: 64px;
}

View File

@@ -59,8 +59,9 @@ final case class Dependency(
license: String,
vulnerabilities: Seq[Vulnerability],
suppressedVulnerabilities: Seq[Vulnerability],
relatedDependencies: SerializableXml
relatedDependencies: Seq[RelatedDependency]
){
def hashes = Hashes(sha1 = sha1, md5 = md5)
def plainLibraryIdentifiers: Set[PlainLibraryIdentifier] = identifiers.flatMap(_.toLibraryIdentifierOption).toSet
@@ -70,6 +71,20 @@ final case class Dependency(
Method equals seems to be a CPU hog there. I am not sure if we can do something reasonable about it.
We can compare by this.hashes, but, in such case, dependencies that differ in evidence will be considered the same if their JAR hashes are the same, which would break some sanity checks.
*/
}
final case class RelatedDependency(
fileName: String,
filePath: String,
md5: String,
sha1: String,
description: String,
identifiers: Seq[Identifier],
suppressedIdentifiers: Seq[Identifier],
license: String,
vulnerabilities: Seq[Vulnerability],
suppressedVulnerabilities: Seq[Vulnerability]
){
}
/**
@@ -189,6 +204,7 @@ object OdcParser {
private val vulnPool = new ObjectPool()
private val evidencePool = new ObjectPool()
private val relatedDependencyPool = new ObjectPool()
private val dependencyPool = new ObjectPool()
private val identifierPool = new ObjectPool()
private val vulnerableSoftwarePool = new ObjectPool()
@@ -285,7 +301,7 @@ object OdcParser {
))
}
def parseIdentifier(node: Node, expectedLabel: String): Identifier = {
def parseIdentifier(node: Node, expectedLabel: String, parseConfidence: Boolean = true): Identifier = {
if(node.label != expectedLabel){
sys.error("Unexpected label for identifier: "+node.label)
}
@@ -298,7 +314,7 @@ object OdcParser {
},
url = (node \ "url").text,
identifierType = node.attribute("type").get.text,
confidence = Confidence.withName(node.attribute("confidence").get.text)
confidence = if(parseConfidence) Confidence.withName(node.attribute("confidence").get.text) else Confidence.Medium
))
}
@@ -319,7 +335,25 @@ object OdcParser {
license = (node \ "license").text,
vulnerabilities = vulnerabilities.map(parseVulnerability(_)),
suppressedVulnerabilities = suppressedVulnerabilities.map(parseVulnerability(_, "suppressedVulnerability")),
relatedDependencies = SerializableXml(node \ "relatedDependencies")
relatedDependencies = (node \ "relatedDependencies" \ "relatedDependency").map(parseRelatedDependency)
))
}
def parseRelatedDependency(node: Node): RelatedDependency = {
checkElements(node, Set("fileName", "filePath", "md5", "sha1", "description", "evidenceCollected", "identifier", "license", "vulnerabilities", "relatedDependencies"))
checkParams(node, Set())
val (vulnerabilities: Seq[Node], suppressedVulnerabilities: Seq[Node]) = (node \ "vulnerabilities").headOption.map(filterWhitespace).getOrElse(Seq()).partition(_.label == "vulnerability")
relatedDependencyPool(RelatedDependency(
fileName = (node \ "fileName").text,
filePath = (node \ "filePath").text,
md5 = (node \ "md5").text,
sha1 = (node \ "sha1").text,
description = (node \ "description").text,
identifiers = (node \ "identifier").map(parseIdentifier(_, "identifier", parseConfidence = false)),
suppressedIdentifiers = (node \ "suppressedIdentifier").map(parseIdentifier(_, "suppressedIdentifier", parseConfidence = false)),
license = (node \ "license").text,
vulnerabilities = vulnerabilities.map(parseVulnerability(_)),
suppressedVulnerabilities = suppressedVulnerabilities.map(parseVulnerability(_, "suppressedVulnerability"))
))
}

View File

@@ -285,7 +285,8 @@ class Application @Inject() (
Ok(
JavaScriptReverseRouter("Routes")(
routes.javascript.Application.setClassified,
routes.javascript.Application.addTag
routes.javascript.Application.addTag,
routes.javascript.LibraryAdvisor.scan
)
).as("text/javascript")
}

View File

@@ -113,6 +113,7 @@ private final case class BadFilter(pattern: String) extends Filter{
}
object DependencyCheckReportsParser{
def forAdHocScan(analysis: Analysis): Result = Result(Map(ReportInfo("adHocScan", "Ad hoc scan", "AHS", None) -> analysis), Map(), new ProjectsWithReports(new Projects(Map(), Map(), Map()), Set()), Map())
final case class ResultWithSelection(result: Result, projectsWithSelection: ProjectsWithSelection)
final case class Result(bareFlatReports: Map[ReportInfo, Analysis], bareFailedAnalysises: Map[ReportInfo, Throwable], projectsReportInfo: ProjectsWithReports/*TODO: maybe rename to rootProjects*/, failedReportDownloads: Map[ReportInfo, Throwable]){
//lazy val projectsReportInfo = new ProjectsWithReports(projects, (bareFlatReports.keySet ++ bareFailedAnalysises.keySet ++ failedReportDownloads.keySet).map(_.fullId)) // TODO: consider renaming to projectsWithReports
@@ -126,6 +127,7 @@ object DependencyCheckReportsParser{
groupedDependencies.toSet.flatMap((grDep: GroupedDependency) => grDep.plainLibraryIdentifiers.map(_ -> grDep)).groupBy(_._1).mapValues(_.map(_._2)).map(identity)
lazy val groupedDependenciesByHashes: Map[Hashes, GroupedDependency] = groupedDependencies.map(gd => gd.hashes -> gd).toMap
lazy val vulnerableDependencies = groupedDependencies.filter(_.vulnerabilities.nonEmpty)
lazy val nonVulnerableDependencies = groupedDependencies.filter(_.vulnerabilities.isEmpty)
lazy val suppressedOnlyDependencies = groupedDependencies.filter(gd => gd.vulnerabilities.isEmpty && gd.suppressedIdentifiers.nonEmpty)
private val ProjectSelectorPattern = """^project:(.*)$""".r

View File

@@ -0,0 +1,114 @@
package controllers
import java.net.URL
import javax.inject.Inject
import com.github.nscala_time.time.Imports._
import com.ysoft.odc.SecureXml
import modules.TemplateCustomization
import org.joda.time.DateTime
import play.api.Configuration
import play.api.i18n.MessagesApi
import play.api.mvc.{Action, AnyContent, Result}
import play.twirl.api.Html
import services.{DependencyNotFoundException, OdcDbService, OdcService, SingleLibraryScanResult}
import views.html.DefaultRequest
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
class LibraryAdvisor @Inject() (
config: Configuration,
odcServiceOption: Option[OdcService],
odcDbService: OdcDbService,
val messagesApi: MessagesApi,
val env: AuthEnv,
val templateCustomization: TemplateCustomization
) (implicit ec: ExecutionContext) extends AuthenticatedController
{
import secureRequestConversion._
private def withOdc(f: OdcService => Future[Result])(implicit defaultRequest: DefaultRequest) = {
odcServiceOption.fold(Future.successful(InternalServerError(views.html.libraryAdvisor.notEnabled())))(odcService =>
f(odcService)
)
}
private val InputParsers = Seq[(OdcService, String) => Option[Either[Future[SingleLibraryScanResult], String]]](
(odcService, xmlString) => {
val triedElem = Try {
SecureXml.loadString(xmlString)
}
triedElem.toOption.map{ xml =>
xml.label match {
case "dependency" =>
/*
Maven POM, e.g.:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.3.1</version>
</dependency>
*/
val groupId = (xml \ "groupId").text
val artifactId = (xml \ "artifactId").text
val version = (xml \ "version").text
Left(odcService.scanMaven(groupId, artifactId, version))
case other =>
Right(s"Unknown root XML element: $other")
}
}
},
(odcService, urlString) => {
Try{new URL(urlString)}.toOption.map{url =>
url.getHost match {
case "www.mvnrepository.com" | "mvnrepository.com" =>
// https://www.mvnrepository.com/artifact/ch.qos.logback/logback-classic/0.9.10
// https://mvnrepository.com/artifact/ch.qos.logback/logback-classic/0.9.10
url.getPath.split('/') match {
case Array("", "artifact", groupId, artifactId, version) =>
Left(odcService.scanMaven(groupId, artifactId, version))
case _ =>
Right("Unknown path for mvnrepository.com: Expected https://mvnrepository.com/artifact/<groupId>/<artifactId>/<version>")
}
}
}
}
)
def index(dependency: Option[String]): Action[AnyContent] = ReadAction.async{ implicit req =>
withOdc{ odcService =>
Future.successful(Ok(views.html.libraryAdvisor.scanLibrary(dependency, Seq(
Html("&lt;dependency>…&lt;/dependency>  Maven POM format"),
Html("https://mvnrepository.com/artifact/<i>groupId</i>/<i>artifactId</i>/<i>version</i>")
))))
}
}
//noinspection TypeAnnotation
def scan() = ReadAction.async(parse.json[String]){ implicit req =>
withOdc{ odcService =>
val now = DateTime.now()
val oldDataThreshold = 2.days
val lastDbUpdateFuture = odcDbService.loadLastDbUpdate()
val isOldFuture = lastDbUpdateFuture.map{ lastUpdate => now - oldDataThreshold > lastUpdate}
val response = InputParsers.toStream.map(_(odcService, req.body)).find(_.nonEmpty).flatten match{
case None => Future.successful(Ok(views.html.libraryAdvisor.scanInputError("Unknown input format")))
case Some(Right(message)) => Future.successful(Ok(views.html.libraryAdvisor.scanInputError(s"Unknown input format: $message")))
case Some(Left(resFuture)) =>
for{
res <- resFuture
isOld <- isOldFuture
} yield Ok(views.html.libraryAdvisor.scanResults(isOld, res))
}
response.recover{
case DependencyNotFoundException(dependency) =>
NotFound(views.html.libraryAdvisor.notFound(dependency))
}.map { _.withHeaders("Content-type" -> "text/plain; charset=utf-8")}
}
}
}

View File

@@ -26,7 +26,7 @@ class Notifications @Inject()(
dependencyCheckReportsParser: DependencyCheckReportsParser,
issueTrackerServiceOption: Option[IssueTrackerService],
emailExportServiceOption: Option[EmailExportService],
odcService: OdcService,
odcService: OdcDbService,
absolutizer: Absolutizer,
val env: AuthEnv,
val templateCustomization: TemplateCustomization

View File

@@ -67,7 +67,8 @@ class Statistics @Inject()(
dependencyCheckReportsParser: DependencyCheckReportsParser,
librariesService: LibrariesService,
tagsService: TagsService,
odcService: OdcService,
odcDbService: OdcDbService,
odcServiceOption: Option[OdcService],
libraryTagAssignmentsService: LibraryTagAssignmentsService,
@Named("missing-GAV-exclusions") missingGAVExclusions: MissingGavExclusions,
projects: Projects,
@@ -98,14 +99,14 @@ class Statistics @Inject()(
}else{
val now = DateTime.now()
val oldDataThreshold = 2.days
val lastDbUpdateFuture = odcService.loadLastDbUpdate()
val lastDbUpdateFuture = odcDbService.loadLastDbUpdate()
val isOldFuture = lastDbUpdateFuture.map{ lastUpdate => now - oldDataThreshold > lastUpdate}
versionOption match {
case Some(version) =>
for {
res1 <- Future.traverse(versionlessCpes) { versionlessCpe => odcService.findRelevantCpes(versionlessCpe, version) }
res1 <- Future.traverse(versionlessCpes) { versionlessCpe => odcDbService.findRelevantCpes(versionlessCpe, version) }
vulnIds = res1.flatten.map(_.vulnerabilityId).toSet
vulns <- Future.traverse(vulnIds)(id => odcService.getVulnerabilityDetails(id).map(_.get))
vulns <- Future.traverse(vulnIds)(id => odcDbService.getVulnerabilityDetails(id).map(_.get))
isOld <- isOldFuture
} yield Ok(views.html.statistics.vulnerabilitiesForLibrary(
vulnsAndVersionOption = Some((vulns, version)),
@@ -213,17 +214,27 @@ class Statistics @Inject()(
}// .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{
for{
vulnOption <- odcService.getVulnerabilityDetails(name)
vulnOption <- odcDbService.getVulnerabilityDetails(name)
issueOption <- issueOptionFuture
} yield Ok(views.html.statistics.vulnerabilityNotFound( // TODO: the not found page might be replaced by some page explaining that there is no project affected by that vulnerability
name = name,
} yield vulnOption.fold(
Ok(views.html.statistics.vulnerabilityNotFound(
name = name,
projectsWithSelection = selection.projectsWithSelection,
failedProjects = selection.result.failedProjects,
issueOption = issueOption
))
)(vuln => Ok(views.html.statistics.vulnerability(
projectsWithSelection = selection.projectsWithSelection,
failedProjects = selection.result.failedProjects,
issueOption = issueOption
))
issueOption = issueOption,
vulnerability = vuln,
affectedProjects = Map(),
affectedLibraries = Set(),
vulnerableDependencies = Set()
)))
}{ vulnerableDependencies =>
for {
vulnOption <- odcService.getVulnerabilityDetails(name)
vulnOption <- odcDbService.getVulnerabilityDetails(name)
plainLibs <- librariesService.byPlainLibraryIdentifiers(vulnerableDependencies.flatMap(_.plainLibraryIdentifiers)).map(_.keySet)
issueOption <- issueOptionFuture
} yield vulnOption.fold{

View File

@@ -1,6 +1,7 @@
import com.mohiva.play.silhouette.api.Environment
import com.mohiva.play.silhouette.impl.authenticators.CookieAuthenticator
import models.{SnoozeInfo, User}
import play.api.mvc.Call
/**
* Created by user on 7/15/15.
@@ -35,4 +36,30 @@ package object controllers {
}*/
def friendlyProjectNameString(reportInfo: ReportInfo) = reportInfo.subprojectNameOption.fold(reportInfo.projectName)(reportInfo.projectName+": "+_)
val severityOrdering: Ordering[GroupedDependency] = Ordering.by((d: GroupedDependency) => (
d.maxCvssScore.map(-_).getOrElse(0.0), // maximum CVSS score is the king
if(d.maxCvssScore.isEmpty) Some(-d.dependencies.size) else None, // more affected dependencies if no vulnerability has defined severity
-d.vulnerabilities.size, // more vulnerabilities
-d.projects.size, // more affected projects
d.cpeIdentifiers.map(_.toCpeIdentifierOption.get).toSeq.sorted.mkString(" ")) // at least make the order deterministic
)
def vulnerableSoftwareSearches(groupedDependency: GroupedDependency): Seq[(Call, String)] = {
val legacySearchOption = groupedDependency.cpeIdentifiers match {
case Seq() => None
case cpeIds => Some(
routes.Statistics.searchVulnerableSoftware(
cpeIds.map(_.name.split(':').take(4).mkString(":")).toSeq, None
) -> "Search by CPE (legacy option)"
)
}
val mavenSearches = groupedDependency.mavenIdentifiers.map(_.name).toSeq.sorted.map{mavenIdentifier =>
val Array(groupId, artifactId, version) = mavenIdentifier.split(":", 3)
val identifierString = <dependency><groupId>{groupId}</groupId><artifactId>{artifactId}</artifactId><version>{version}</version></dependency>.toString()
routes.LibraryAdvisor.index(Some(identifierString)) -> s"Look for Maven dependency $mavenIdentifier"
}
mavenSearches ++ legacySearchOption
}
}

View File

@@ -6,7 +6,7 @@ import java.nio.file.{Files, Path, Paths}
import java.util.concurrent.Executors
import akka.util.ClassLoaderObjectInputStream
import com.typesafe.config.{Config, ConfigObject, ConfigValue}
import com.typesafe.config.{Config, ConfigObject}
import com.ysoft.odc._
import controllers.api._
import controllers.{MissingGavExclusions, Projects, TeamId, WarningSeverity}
@@ -15,9 +15,7 @@ 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.collection.mutable
import scala.concurrent.ExecutionContext
import scala.concurrent.duration.Duration
import scala.reflect.ClassTag
@@ -86,7 +84,7 @@ class FileCacheApi(path: Path) extends CacheApi{
}
case class TemplateCustomization(brandHtml: Option[String])
case class TemplateCustomization(brandHtml: Option[String], vulnerableLibraryAdvice: Option[String])
class ConfigModule extends Module {
@@ -158,7 +156,7 @@ class ConfigModule extends Module {
bind[LogSmellChecks].qualifiedWith("log-smells").toInstance(LogSmellChecks(configuration.underlying.getAs[Map[String, LogSmell]]("yssdc.logSmells").getOrElse(Map()))),
bind[Projects].to(parseProjects(configuration)),
bind[ApiConfig].to(parseApiConfig(configuration)),
bind[TemplateCustomization].to(TemplateCustomization(configuration.underlying.getAs[String]("app.brand")))
bind[TemplateCustomization].to(TemplateCustomization(configuration.underlying.getAs[String]("app.brand"), configuration.underlying.getAs[String]("app.vulnerableLibraryAdvice")))
) ++
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)))) ++

View File

@@ -8,7 +8,7 @@ import net.ceedubs.ficus.Ficus._
import net.codingwell.scalaguice.ScalaModule
import play.api.Configuration
import play.api.libs.mailer.MailerClient
import services.{OdcService, EmailExportService, EmailExportType, VulnerabilityNotificationService}
import services.{OdcDbService, EmailExportService, EmailExportType, VulnerabilityNotificationService}
import net.ceedubs.ficus.readers.EnumerationReader._
import scala.concurrent.ExecutionContext
@@ -22,7 +22,7 @@ class EmailExportModule extends AbstractModule with ScalaModule{
mailerClient: MailerClient,
notificationService: VulnerabilityNotificationService,
absolutizer: Absolutizer,
odcService: OdcService,
odcService: OdcDbService,
@Named("email-sending") emailSendingExecutionContext: ExecutionContext
)(implicit executionContext: ExecutionContext): Option[EmailExportService] = {
println(s"emailSendingExecutionContext = $emailSendingExecutionContext")

View File

@@ -0,0 +1,33 @@
package modules
import com.google.inject.{AbstractModule, Provides}
import net.ceedubs.ficus.Ficus._
import net.ceedubs.ficus.readers.ArbitraryTypeReader._
import net.codingwell.scalaguice.ScalaModule
import play.api.{Application, Configuration}
import services.{OdcConfig, OdcDbConnectionConfig, OdcService}
class OdcModule extends AbstractModule with ScalaModule{
override def configure(): Unit = {}
private val Drivers = Map(
"slick.driver.MySQLDriver$" -> "org.mariadb.jdbc.Driver"
)
@Provides
def provideOdcServiceOption(conf: Configuration, application: Application): Option[OdcService] = {
lazy val dbConfig = {
val driverClass = Drivers(conf.getString("slick.dbs.odc.driver").get)
val driverJar = Class.forName(driverClass).getProtectionDomain.getCodeSource.getLocation.getPath
OdcDbConnectionConfig(
driverClass = driverClass,
driverJar = driverJar,
url = conf.getString("slick.dbs.odc.db.url").get,
user = conf.getString("slick.dbs.odc.db.user").get,
password = conf.getString("slick.dbs.odc.db.password").get
)
}
conf.underlying.getAs[OdcConfig]("odc").map(config => new OdcService(config, dbConfig)(application))
}
}

View File

@@ -0,0 +1,5 @@
package services
case class DependencyNotFoundException(val dependency: String) extends Exception(s"Dependency $dependency is not found"){
}

View File

@@ -19,7 +19,7 @@ object EmailExportType extends Enumeration {
val Digest = Value("digest")
}
class EmailExportService(from: String, nobodyInterestedContact: String, val exportType: EmailExportType.Value, odcService: OdcService, mailerClient: MailerClient, notificationService: VulnerabilityNotificationService, emailSendingExecutionContext: ExecutionContext, absolutizer: Absolutizer)(implicit executionContext: ExecutionContext) {
class EmailExportService(from: String, nobodyInterestedContact: String, val exportType: EmailExportType.Value, odcService: OdcDbService, mailerClient: MailerClient, notificationService: VulnerabilityNotificationService, emailSendingExecutionContext: ExecutionContext, absolutizer: Absolutizer)(implicit executionContext: ExecutionContext) {
// Maybe it is not the best place for exportType, but I am not sure if we want this to be configurable. If no, then we can get rid of it. If yes, we should refactor it.

View File

@@ -0,0 +1,132 @@
package services
import java.lang.{Boolean => JBoolean}
import java.sql.{Array => _, _}
import java.util.{Properties, Map => JMap}
import _root_.org.owasp.dependencycheck.data.nvdcve.CveDB
import _root_.org.owasp.dependencycheck.dependency.{VulnerableSoftware => OdcVulnerableSoftware}
import _root_.org.owasp.dependencycheck.utils.{DependencyVersion, DependencyVersionUtil, Settings}
import com.github.nscala_time.time.Imports._
import com.google.inject.Inject
import com.mockrunner.mock.jdbc.MockConnection
import models.odc.tables._
import models.odc.{OdcProperty, Vulnerabilities}
import play.api.Logger
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
import play.db.NamedDatabase
import scala.concurrent.{ExecutionContext, Future}
class OdcDbService @Inject()(@NamedDatabase("odc") protected val dbConfigProvider: DatabaseConfigProvider)(implicit executionContext: ExecutionContext) extends HasDatabaseConfigProvider[models.odc.profile.type]{
import dbConfig.driver.api._
private def getVulnerableSoftware(id: Int): Future[Seq[com.ysoft.odc.VulnerableSoftware]] = {
db.run(softwareVulnerabilities.joinLeft(cpeEntries).on((sv, ce) => sv.cpeEntryId === ce.id).filter{case (sv, ceo) => sv.vulnerabilityId === id}.result).map{rawRefs =>
rawRefs.map{
case (softVuln, Some((_, cpeEntry))) => com.ysoft.odc.VulnerableSoftware(allPreviousVersion = softVuln.includesAllPreviousVersions, name=cpeEntry.cpe)
}
}
}
private def getReferences(id: Int): Future[Seq[com.ysoft.odc.Reference]] = db.run(references.filter(_.cveId === id).map(_.base).result)
def getVulnerabilityDetails(id: Int): Future[Option[com.ysoft.odc.Vulnerability]] = getVulnerabilityDetails(_.id === id)
def getVulnerabilityDetails(name: String): Future[Option[com.ysoft.odc.Vulnerability]] = getVulnerabilityDetails(_.cve === name)
private def getVulnerabilityDetails(cond: Vulnerabilities => Rep[Boolean]): Future[Option[com.ysoft.odc.Vulnerability]] = {
db.run(vulnerabilities.filter(cond).result).map(_.headOption) flatMap { bareVulnOption =>
bareVulnOption.fold[Future[Option[com.ysoft.odc.Vulnerability]]](Future.successful(None)) { case (id, bareVuln) =>
for {
vulnerableSoftware <- getVulnerableSoftware(id)
references <- getReferences(id)
} yield Some(
com.ysoft.odc.Vulnerability(
name = bareVuln.cve,
cweOption = bareVuln.cweOption,
cvss = bareVuln.cvss,
description = bareVuln.description,
vulnerableSoftware = vulnerableSoftware,
references = references
)
)
}
}
}
private def parseCpe(cpe: String) = {
val sw = new OdcVulnerableSoftware()
sw.parseName(cpe)
sw
}
private def parseVersion(version: String): DependencyVersion = {
DependencyVersionUtil.parseVersion(version)
}
def findRelevantCpes(versionlessCpe: String, version: String) = {
println(s"versionlessCpe: $versionlessCpe")
val Seq("cpe", "/a", vendor, product, rest @ _*) = versionlessCpe.split(':').toSeq
val cpesFuture = db.run(
cpeEntries.filter(c =>
c.vendor === vendor && c.product === product
).result
)
for(cpes <- cpesFuture){println(s"cpes: $cpes")}
val cpesMapFuture = cpesFuture.map(_.toMap)
val cpeIdsFuture = cpesFuture.map(_.map(_._1))
val parsedVersion = parseVersion(version)
val res = for{
cpeIds <- cpeIdsFuture
relevantVulnerabilities <- db.run(
softwareVulnerabilities.join(vulnerabilities).on( (sv, v) => sv.vulnerabilityId === v.id)
.filter{case (sv, v) => sv.cpeEntryId inSet cpeIds}.map{case (sv, v) sv}.result
).map(_.groupBy(_.vulnerabilityId).mapValues(_.toSet))
cpesMap <- cpesMapFuture
//relevantVulnerabilities <- db.run(vulnerabilities.filter(_.id inSet relevantVulnerabilityIds).result)
} yield relevantVulnerabilities.filter{case (vulnId, sv) => Option(CveDbHelper.matchSofware(
vulnerableSoftware = sv.map(sv => cpesMap(sv.cpeEntryId).cpe -> sv.includesAllPreviousVersions).toMap,
vendor = vendor,
product = product,
identifiedVersion = parsedVersion
)).isDefined}
res.map(_.values.toSet.flatten)
}
private def loadUpdateProperties(): Future[Map[String, Long]] = db.run(properties.filter(_.id like "NVD CVE%").result).map(_.map{case OdcProperty(id, value) => (id, value.toLong)}.toMap)
def loadLastDbUpdate(): Future[DateTime] = loadUpdateProperties().map(vals => new DateTime(vals.values.max)) // TODO: timezone (I don't care much, though)
}
private[services] object CveDbHelper {
class DummyDriver extends Driver{
override def acceptsURL(url: String): Boolean = {url.startsWith("jdbc:dummy:")}
override def jdbcCompliant(): Boolean = false
override def connect(url: String, info: Properties): Connection = new MockConnection()
override def getParentLogger = throw new SQLFeatureNotSupportedException()
override def getPropertyInfo(url: String, info: Properties): Array[DriverPropertyInfo] = {Array()}
override def getMinorVersion: Int = 1
override def getMajorVersion: Int = 1
}
org.apache.geronimo.jdbc.DelegatingDriver.registerDriver(new DummyDriver())
def matchSofware(vulnerableSoftware: Map[String, Boolean], vendor: String, product: String, identifiedVersion: DependencyVersion) = {
if(Settings.getInstance() == null){
Settings.initialize()// Initiallize ODC environment on first use; Needed for each thread.
Settings.setString(Settings.KEYS.DB_CONNECTION_STRING, "jdbc:dummy:")
// Workaround: At first initialization, it will complain that the DB is empty. On next initializations, it will not complain.
try{new CveDB()}catch {case e: Throwable => Logger.info("A workaround-related exception, safe to ignore", e)}
}
val cd = new CveDB()
import scala.collection.JavaConversions._
val method = cd.getClass.getDeclaredMethod("getMatchingSoftware", classOf[JMap[String, JBoolean]], classOf[String], classOf[String], classOf[DependencyVersion])
method.setAccessible(true)
method.invoke(cd, mapAsJavaMap(vulnerableSoftware).asInstanceOf[JMap[String, JBoolean]], vendor, product, identifiedVersion)
}
}

View File

@@ -1,148 +1,245 @@
package services
import java.io.File.separatorChar
import java.io._
import java.lang.{Boolean => JBoolean}
import java.sql.{Array => _, _}
import java.nio.charset.StandardCharsets.UTF_8
import java.nio.file._
import java.nio.file.attribute.BasicFileAttributes
import java.sql.{Array => _}
import java.util.{Properties, Map => JMap}
import _root_.org.owasp.dependencycheck.data.nvdcve.CveDB
import _root_.org.owasp.dependencycheck.dependency.VulnerableSoftware
import _root_.org.owasp.dependencycheck.utils.{DependencyVersion, DependencyVersionUtil, Settings}
import com.github.nscala_time.time.Imports._
import _root_.org.apache.commons.lang3.SystemUtils
import _root_.org.owasp.dependencycheck.dependency.{VulnerableSoftware => OdcVulnerableSoftware}
import com.google.inject.Inject
import com.mockrunner.mock.jdbc.MockConnection
import models.odc.tables._
import models.odc.{OdcProperty, Vulnerabilities}
import play.api.Logger
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
import play.db.NamedDatabase
import com.ysoft.odc.{GroupedDependency, Identifier, OdcParser}
import controllers.DependencyCheckReportsParser
import play.api.Application
import play.api.libs.concurrent.Akka
import scala.concurrent.{ExecutionContext, Future}
class OdcService @Inject()(@NamedDatabase("odc") protected val dbConfigProvider: DatabaseConfigProvider)(implicit executionContext: ExecutionContext) extends HasDatabaseConfigProvider[models.odc.profile.type]{
case class OdcDbConnectionConfig(driverClass: String, driverJar: String, url: String, user: String, password: String)
import dbConfig.driver.api._
case class OdcConfig(odcPath: String, extraArgs: Seq[String] = Seq(), workingDirectory: String = ".", propertyFile: Option[String])
def getVulnerableSoftware(id: Int): Future[Seq[com.ysoft.odc.VulnerableSoftware]] = {
db.run(softwareVulnerabilities.joinLeft(cpeEntries).on((sv, ce) => sv.cpeEntryId === ce.id).filter{case (sv, ceo) => sv.vulnerabilityId === id}.result).map{rawRefs =>
rawRefs.map{
case (softVuln, Some((_, cpeEntry))) => com.ysoft.odc.VulnerableSoftware(allPreviousVersion = softVuln.includesAllPreviousVersions, name=cpeEntry.cpe)
case class SingleLibraryScanResult(mainDependency: GroupedDependency, transitiveDependencies: Seq[GroupedDependency], includesTransitive: Boolean)
class OdcService @Inject() (odcConfig: OdcConfig, odcDbConnectionConfig: OdcDbConnectionConfig)(implicit application: Application){
private implicit val executionContext: ExecutionContext = Akka.system.dispatchers.lookup("contexts.odc-workers")
private def suffix = if(SystemUtils.IS_OS_WINDOWS) "bat" else "sh"
private def odcBin = new File(new File(odcConfig.odcPath), "bin"+separatorChar+"dependency-check."+suffix).getAbsolutePath
private def mavenBin = "mvn"
private val OutputFormat = "XML"
private val DependencyNotFoundPrefix = "[ERROR] Failed to execute goal on project odc-adhoc-project: Could not resolve dependencies for project com.ysoft:odc-adhoc-project:jar:1.0-SNAPSHOT: Could not find artifact "
private def mavenLogChecks(log: String) = {
if(log.lines contains "[INFO] No dependencies were identified that could be analyzed by dependency-check"){
sys.error("Dependency not identified. Log: "+log)
}
for(missingDependencyMessage <- log.lines.find(_.startsWith(DependencyNotFoundPrefix))){
val missingDependency = missingDependencyMessage.drop(DependencyNotFoundPrefix.length).takeWhile(_ != ' ')
throw DependencyNotFoundException(missingDependency)
}
}
def scanMaven(groupId: String, artifactId: String, version: String): Future[SingleLibraryScanResult] = scanInternal(
createOdcCommand = createMavenOdcCommand,
isMainLibraryOption = Some(_.exists(id => id.identifierType == "maven" && id.name == s"$groupId:$artifactId:$version")),
logChecks = mavenLogChecks
){ dir =>
val pomXml = <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ysoft</groupId>
<artifactId>odc-adhoc-project</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<configuration>
<outputDirectory>{"${outputDirectory}"}</outputDirectory>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>{groupId}</groupId>
<artifactId>{artifactId}</artifactId>
<version>{version}</version>
</dependency>
</dependencies>
</project>
Files.write(dir.resolve("pom.xml"), pomXml.toString.getBytes(UTF_8))
}
private def consumeStream(in: InputStream): Array[Byte] = {
val baos = new ByteArrayOutputStream()
val buff = new Array[Byte](1024)
var size: Int = 0
while({size = in.read(buff); size != -1}) {
baos.write(buff, 0, size)
}
baos.toByteArray
}
private def scanInternal(
createOdcCommand: (String, Path, String) => Seq[String] = createStandardOdcCommand,
isMainLibraryOption: Option[Seq[Identifier] => Boolean],
logChecks: String => Unit = s => ()
)(
f: Path => Unit
): Future[SingleLibraryScanResult] = Future{
withTmpDir { scanDir =>
val scandirPrefix = s"$scanDir$separatorChar"
val reportFilename = s"${scandirPrefix}report.xml"
val path = scanDir.resolve("scanned-dir")
Files.createDirectory(path)
f(path)
val cmd: Seq[String] = createOdcCommand(scandirPrefix, path, reportFilename)
val process = new ProcessBuilder(cmd: _*).
directory(new File(odcConfig.workingDirectory)).
redirectErrorStream(true).
start()
val in = process.getInputStream
// We consume all output in order not to hang; we mix stderr and stdout together
val outArray = consumeStream(in)
val res = process.waitFor()
lazy val log = new String(outArray)
logChecks(log)
if(res != 0){
sys.error(s"Non-zero return value: $res; output: $log")
}
val result = DependencyCheckReportsParser.forAdHocScan(OdcParser.parseXmlReport(Files.readAllBytes(Paths.get(reportFilename))))
val (Seq(mainLibrary), otherLibraries) = result.allDependencies.partition{case (dep, _) => isMainLibraryOption.fold(true)(f => f(dep.identifiers) || dep.relatedDependencies.map(_.identifiers).exists(f))}
SingleLibraryScanResult(
mainDependency = GroupedDependency(Seq(mainLibrary)),
transitiveDependencies = otherLibraries.map(dep => GroupedDependency(Seq(dep))),
includesTransitive = isMainLibraryOption.isDefined
)
}
}
private def odcVersion: String = {
import sys.process._
Seq(odcBin, "--version").!!.trim.reverse.takeWhile(_!=' ').reverse
}
private def createHintfulOdcCommand(scandirPrefix: String, path: Path, reportFilename: String): Seq[String] = {
val newPropertyFile = s"${scandirPrefix}odc.properties"
val p = new Properties()
for(origPropFile <- odcConfig.propertyFile){
val in = new FileInputStream(Paths.get(odcConfig.workingDirectory).resolve(origPropFile).toFile)
try{
p.load(in)
}finally{
in.close()
}
}
}
def getReferences(id: Int): Future[Seq[com.ysoft.odc.Reference]] = db.run(references.filter(_.cveId === id).map(_.base).result)
def getVulnerabilityDetails(id: Int): Future[Option[com.ysoft.odc.Vulnerability]] = getVulnerabilityDetails(_.id === id)
def getVulnerabilityDetails(name: String): Future[Option[com.ysoft.odc.Vulnerability]] = getVulnerabilityDetails(_.cve === name)
def getVulnerabilityDetails(cond: Vulnerabilities => Rep[Boolean]): Future[Option[com.ysoft.odc.Vulnerability]] = {
db.run(vulnerabilities.filter(cond).result).map(_.headOption) flatMap { bareVulnOption =>
bareVulnOption.fold[Future[Option[com.ysoft.odc.Vulnerability]]](Future.successful(None)) { case (id, bareVuln) =>
for {
vulnerableSoftware <- getVulnerableSoftware(id)
references <- getReferences(id)
} yield Some(
com.ysoft.odc.Vulnerability(
name = bareVuln.cve,
cweOption = bareVuln.cweOption,
cvss = bareVuln.cvss,
description = bareVuln.description,
vulnerableSoftware = vulnerableSoftware,
references = references
)
)
}
}
}
private def parseCpe(cpe: String) = {
val sw = new VulnerableSoftware()
sw.parseName(cpe)
sw
}
def parseVersion(version: String): DependencyVersion = {
DependencyVersionUtil.parseVersion(version)
}
/*def parseCpeVersion(cpe: String): DependencyVersion = { // strongly inspired by org.owasp.dependencycheck.data.nvdcve.CveDB.parseDependencyVersion(cpe: VulnerableSoftware): DependencyVersion
def StringOption(s: String) = Option(s).filterNot(_.isEmpty)
val sw = parseCpe(cpe)
StringOption(sw.getVersion) match {
case None ⇒ new DependencyVersion("-")
case Some(bareVersionString) ⇒
DependencyVersionUtil.parseVersion(
StringOption(sw.getUpdate) match {
case None ⇒ bareVersionString
case Some(update) ⇒ s"$bareVersionString.$update"
}
)
}
}*/
def findRelevantCpes(versionlessCpe: String, version: String) = {
println(s"versionlessCpe: $versionlessCpe")
val Seq("cpe", "/a", vendor, product, rest @ _*) = versionlessCpe.split(':').toSeq
val cpesFuture = db.run(
cpeEntries.filter(c =>
c.vendor === vendor && c.product === product
).result
)
for(cpes <- cpesFuture){println(s"cpes: $cpes")}
val cpesMapFuture = cpesFuture.map(_.toMap)
val cpeIdsFuture = cpesFuture.map(_.map(_._1))
val parsedVersion = parseVersion(version)
val res = for{
cpeIds <- cpeIdsFuture
relevantVulnerabilities <- db.run(
softwareVulnerabilities.join(vulnerabilities).on( (sv, v) => sv.vulnerabilityId === v.id)
.filter{case (sv, v) => sv.cpeEntryId inSet cpeIds}.map{case (sv, v) sv}.result
).map(_.groupBy(_.vulnerabilityId).mapValues(_.toSet))
cpesMap <- cpesMapFuture
//relevantVulnerabilities <- db.run(vulnerabilities.filter(_.id inSet relevantVulnerabilityIds).result)
} yield relevantVulnerabilities.filter{case (vulnId, sv) => Option(CveDbHelper.matchSofware(
vulnerableSoftware = sv.map(sv => cpesMap(sv.cpeEntryId).cpe -> sv.includesAllPreviousVersions).toMap,
vendor = vendor,
product = product,
identifiedVersion = parsedVersion
)).isDefined}
res.map(_.values.toSet.flatten)
}
def loadUpdateProperties(): Future[Map[String, Long]] = db.run(properties.filter(_.id like "NVD CVE%").result).map(_.map{case OdcProperty(id, value) => (id, value.toLong)}.toMap)
def loadLastDbUpdate(): Future[DateTime] = loadUpdateProperties().map(vals => new DateTime(vals.values.max)) // TODO: timezone (I don't care much, though)
}
private[services] object CveDbHelper {
class DummyDriver extends Driver{
override def acceptsURL(url: String): Boolean = {url.startsWith("jdbc:dummy:")}
override def jdbcCompliant(): Boolean = false
override def connect(url: String, info: Properties): Connection = new MockConnection()
override def getParentLogger = throw new SQLFeatureNotSupportedException()
override def getPropertyInfo(url: String, info: Properties): Array[DriverPropertyInfo] = {Array()}
override def getMinorVersion: Int = 1
override def getMajorVersion: Int = 1
}
org.apache.geronimo.jdbc.DelegatingDriver.registerDriver(new DummyDriver())
def matchSofware(vulnerableSoftware: Map[String, Boolean], vendor: String, product: String, identifiedVersion: DependencyVersion) = {
if(Settings.getInstance() == null){
Settings.initialize()// Initiallize ODC environment on first use; Needed for each thread.
Settings.setString(Settings.KEYS.DB_CONNECTION_STRING, "jdbc:dummy:")
// Workaround: At first initialization, it will complain that the DB is empty. On next initializations, it will not complain.
try{new CveDB()}catch {case e: Throwable => Logger.info("A workaround-related exception, safe to ignore", e)}
}
val cd = new CveDB()
import scala.collection.JavaConversions._
val method = cd.getClass.getDeclaredMethod("getMatchingSoftware", classOf[JMap[String, JBoolean]], classOf[String], classOf[String], classOf[DependencyVersion])
method.setAccessible(true)
method.invoke(cd, mapAsJavaMap(vulnerableSoftware).asInstanceOf[JMap[String, JBoolean]], vendor, product, identifiedVersion)
p.put("hints.file", s"${scandirPrefix}hints.xml")
p.putAll(dbProps)
val out = new FileOutputStream(Paths.get(newPropertyFile).toFile)
try{
p.store(out, "no comment")
}finally {
out.close()
}
val cmdBase = Seq(
odcBin,
"-s", path.toString,
"--project", "AdHocProject",
"--noupdate",
"-f", OutputFormat,
"-l", s"${scandirPrefix}verbose.log",
"--out", reportFilename,
"-P", newPropertyFile
)
cmdBase ++ odcConfig.extraArgs
}
}
private def createStandardOdcCommand(scandirPrefix: String, path: Path, reportFilename: String): Seq[String] = {
val cmdBase = Seq(
odcBin,
"-s", path.toString,
"--project", "AdHocProject",
"--noupdate",
"-f", OutputFormat,
"-l", s"${scandirPrefix}verbose.log",
"--out", reportFilename
) ++ odcConfig.propertyFile.fold(Seq[String]())(Seq("-P", _))
cmdBase ++ odcConfig.extraArgs
}
private def createMavenOdcCommand(scandirPrefix: String, path: Path, reportFilename: String): Seq[String] = {
val cmdBase = Seq(
mavenBin,
"--file", s"${path}${separatorChar}pom.xml",
"-X",
"-U", // force update
s"org.owasp:dependency-check-maven:$odcVersion:check",
"-Dautoupdate=false",
s"-Dformat=$OutputFormat",
s"-DlogFile=${scandirPrefix}verbose.log",
s"-DoutputDirectory=$reportFilename"
)
cmdBase ++ propsArgs ++ propsToArgs(dbProps) // TODO: fix credentials leak via /proc
}
private def dbProps = Map(
"data.driver_path" -> odcDbConnectionConfig.driverJar,
"data.driver_name" -> odcDbConnectionConfig.driverClass,
"data.connection_string" -> odcDbConnectionConfig.url,
"data.user" -> odcDbConnectionConfig.user,
"data.password" -> odcDbConnectionConfig.password
)
private def propsToArgs(props: Traversable[(String, String)]): Traversable[String] = for((key, value) <- props) yield s"-D$key=$value"
private def propsArgs = odcConfig.propertyFile.fold(Seq[String]()){ propertyFile =>
val props = new Properties()
val in = new FileInputStream(Paths.get(odcConfig.workingDirectory).resolve(propertyFile).toFile)
try {
props.load(in)
} finally {
in.close()
}
import scala.collection.JavaConversions._
propsToArgs(props.toSeq).toSeq
}
private def withTmpDir[T](f: Path => T): T = {
val tmpDir = Files.createTempDirectory("odc")
try {
f(tmpDir)
} finally {
rmdir(tmpDir)
}
}
private def rmdir(dir: Path) = {
Files.walkFileTree(dir, new SimpleFileVisitor[Path] {
override def visitFile(f: Path, basicFileAttributes: BasicFileAttributes): FileVisitResult = {
Files.delete(f)
FileVisitResult.CONTINUE
}
override def postVisitDirectory(dir: Path, e: IOException): FileVisitResult = {
Files.delete(dir)
FileVisitResult.CONTINUE
}
})
}
override def toString = s"OdcService($odcConfig, $executionContext)"
}

View File

@@ -1,13 +1,25 @@
@(depPrefix: String, dep: GroupedDependency, selectorOption: Option[String])
@(depPrefix: String, dep: GroupedDependency, selectorOption: Option[String], showAffectedProjects: Boolean = false, expandVulnerabilities: Boolean = false, vulnerabilitySearch: Boolean = true)
@dep.cpeIdentifiers.toSeq match {
case Seq() => {}
case cpeIds => {
<p>
<a href="@routes.Statistics.searchVulnerableSoftware(
cpeIds.map(_.name.split(':').take(4).mkString(":")).toSeq, None
)" title="Search for known vulnerabilities" class="btn btn-default">Look for vulnerabilities in other versions</a>
</p>
@if(vulnerabilitySearch){
@vulnerableSoftwareSearches(dep) match {
case Seq() => {}
case Seq((link, description)) => {
<p>
<a href="@link" title="Search for known vulnerabilities" class="btn btn-default">Look for vulnerabilities in other versions</a>
</p>
}
case options => {
<p>
<div class="dropdown">
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">Look for vulnerabilities in other versions <span class="caret"></span></button>
<ul class="dropdown-menu">
@for((link, description) <- options){
<li><a href="@link">@description</a></li>
}
</ul>
</div>
</p>
}
}
}
@@ -62,30 +74,32 @@
}
</table>
</div>
<h4 class="expandable" data-toggle="collapse" data-target="#@depPrefix-projects-details">Affected projects (@dep.projects.size)</h4>
<div id="@depPrefix-projects-details" class="collapse in">
<ul>
@for(p <- dep.projects.toIndexedSeq.sorted){
<li>@friendlyProjectName(p)</li>
@if(showAffectedProjects){
<h4 class="expandable" data-toggle="collapse" data-target="#@depPrefix-projects-details">Affected projects (@dep.projects.size)</h4>
<div id="@depPrefix-projects-details" class="collapse in">
<ul>
@for(p <- dep.projects.toIndexedSeq.sorted){
<li>@friendlyProjectName(p)</li>
}
</ul>
@if(selectorOption.isDefined){
<h5 class="expandable collapsed sublist" data-toggle="collapse" data-target="#@depPrefix-projects-all-details">All affected projects (including those that aren't included by the filter)</h5>
<div id="@depPrefix-projects-all-details" class="collapse" data-lazyload-url="@routes.Statistics.affectedProjects(
depId = dep.hashes
)"></div>
}
</ul>
@if(selectorOption.isDefined){
<h5 class="expandable collapsed sublist" data-toggle="collapse" data-target="#@depPrefix-projects-all-details">All affected projects (including those that aren't included by the filter)</h5>
<div id="@depPrefix-projects-all-details" class="collapse" data-lazyload-url="@routes.Statistics.affectedProjects(
depId = dep.hashes
)"></div>
}
</div>
</div>
}
<h4 class="expandable" data-toggle="collapse" data-target="#@depPrefix-vulnerabilities-details">Vulnerabilities (@dep.vulnerabilities.size)</h4>
<ul id="@depPrefix-vulnerabilities-details" class="collapse in vulnerabilities-details">
@for(vuln <- dep.vulnerabilities.toSeq.sortBy(_.cvssScore.map(-_)); vulnPrefix = s"$depPrefix-vulnerabilities-details-${vuln.name}"){
<li>
<h5 data-toggle="collapse" class="expandable collapsed" data-target="#@vulnPrefix-details">
<h5 data-toggle="collapse" class="expandable@if(!expandVulnerabilities){ collapsed}" data-target="#@vulnPrefix-details">
@vuln.name
<a href="@routes.Statistics.vulnerability(vuln.name, selectorOption)" target="_blank" onclick="event.stopPropagation();"><span class="glyphicon glyphicon-new-window"></span></a>
@if(vuln.likelyMatchesOnlyWithoutVersion(dep.identifiers)){<span class="warning-expandable" title="Heuristics suspect false positive. Double check <b>what version</b> does this vulnerability apply to, please. It seems that the vulnerability database does not provide enough information to check it automatically." onmouseover="$(this).tooltip({placement: 'right', html:true}).tooltip('show');"></span>}
</h5>
<div id="@vulnPrefix-details" class="collapse vulnerability-expandable">
<div id="@vulnPrefix-details" class="collapse vulnerability-expandable@if(expandVulnerabilities){ in}">
@vulnerability("h6", depPrefix+"-"+vuln.name, vuln)
<p><a class="btn btn-primary more" target="_blank" href="@routes.Statistics.vulnerability(vuln.name, selectorOption)">Full details about this vulnerability</a></p>
</div>

View File

@@ -1,4 +1,4 @@
@(idPrefix: String, list: Seq[GroupedDependency], selectorOption: Option[String], expandByDefault: Boolean = true, addButtons: Boolean = true)
@(idPrefix: String, list: Seq[GroupedDependency], selectorOption: Option[String], lazyLoad: Boolean = true, expand: GroupedDependency => Boolean = _ => false, addButtons: Boolean = true, showAffectedProjects: Boolean = true, expandVulnerabilities: Boolean = false, vulnerabilitySearch: Boolean = true)
@cpeHtmlId(cpe: String) = @{
cpe.getBytes("utf-8").mkString("-")
}
@@ -15,11 +15,15 @@
@for(dep <- list; depPrefix = s"$idPrefix-${dep.hashes.serialized}"){
<tr>
<td class="severity">
@for(s <- dep.maxCvssScore) {
@dep.maxCvssScore.fold{
<span class="label label-success">OK</span>
}{ s =>
<span class="score-vulnerability">@s</span>
<span class="computation-details">
<span class="score-projects">affects @dep.projects.size @if(dep.projects.size>1){projects}else{project}</span>
</span>
@if(showAffectedProjects){
<span class="computation-details">
<span class="score-projects">affects @dep.projects.size @if(dep.projects.size>1){projects}else{project}</span>
</span>
}
}
</td>
<td class="identifiers">
@@ -27,14 +31,18 @@
</td>
<td class="vulns">@for(s <- dep.maxCvssScore) {@dep.vulnerabilities.size}</td>
<td class="actions">
<button data-toggle="collapse" data-target="#@depPrefix-details" class="btn btn-info collapsed expandable expandable-right"></button>
<button data-toggle="collapse" data-target="#@depPrefix-details" class="btn btn-info @if(!expand(dep)){collapsed} expandable expandable-right"></button>
</td>
</tr>
<tr data-wrapper="<td colspan='4'></td>" id="@depPrefix-details" class="details collapse" data-lazyload-url="@routes.Statistics.dependencyDetails(
<tr data-wrapper="<td colspan='4'></td>" id="@depPrefix-details" class="details collapse@if(expand(dep)){ in}" @if(lazyLoad){data-lazyload-url="@routes.Statistics.dependencyDetails(
depPrefix = depPrefix,
depId = dep.hashes,
selectorOption = selectorOption
)"></tr>
)"}>
@if(!lazyLoad){
<td colspan="4">@dependencyDetailsInner(depPrefix = depPrefix, dep = dep, selectorOption = selectorOption, showAffectedProjects = showAffectedProjects, expandVulnerabilities = expandVulnerabilities, vulnerabilitySearch = vulnerabilitySearch)</td>
}
</tr>
}
</table>
<script type="text/javascript">

View File

@@ -0,0 +1,7 @@
@()(implicit header: DefaultRequest, mainTemplateData: MainTemplateData)
@main(
title = s"OWASP Dependency Check not configured"
){
<div class="alert alert-danger">OWASP Dependency Check is not configured, so you cannot run it right now. Contact your administrator, please.</div>
}

View File

@@ -0,0 +1,2 @@
@(dependency: String)
<div class="alert alert-danger">Dependency @dependency was not found.</div>

View File

@@ -0,0 +1,3 @@
@(message: String)(implicit header: DefaultRequest, mainTemplateData: MainTemplateData)
<div class="alert alert-danger">I did not scan the library, because I was unable to understand what library you mean. @message</div>

View File

@@ -0,0 +1,82 @@
@(identifier: Option[String], inputHints: Seq[Html])(implicit header: DefaultRequest, mainTemplateData: MainTemplateData)
@main(
title = s"Library check"
){
<script type="text/javascript">
var LibraryAdvisorUI = {
scan: function(){
var submitButton = $("#submit-button");
var resultsArea = $("#scan-results");
var identifierArea = $("#library-identifier");
var identifier = identifierArea.val();
function disableSubmit(){
submitButton.attr({disabled: true});
identifierArea.attr({disabled: true});
}
function enableSubmit(){
submitButton.attr({disabled: false});
identifierArea.attr({disabled: false});
}
disableSubmit();
resultsArea.html($('<div class="progress">')
.append(
$('<div class="progress-bar progress-bar-striped active" role="progressbar" style="width: 100%;">Scanning, please wait a minute…</div>')
)
);
$.ajax({
url: Routes.controllers.LibraryAdvisor.scan().url,
data: JSON.stringify(identifier),
method: 'POST',
dataType: "text",
contentType : 'application/json',
success: function(res){
resultsArea.html(res);
enableSubmit();
},
error: function(x, e){
if(x.status === 404){
resultsArea.html(x.responseText);
}else{
resultsArea.html($('<div class="alert alert-danger">An error has happened during scan. Check logs for more information.</div>'))
console.log("error", e)
}
enableSubmit();
}
});
}
};
$(function(){
$('#library-identifier').keydown(function (e) {
var isEnter = (e.keyCode === 13 || e.keyCode === 10);
if (isEnter && !e.shiftKey) { // capture enter, pass shift+enter
LibraryAdvisorUI.scan();
};
}).on("input", function(){
var $this = $(this);
$this.scrollTop($this.height());
});
$('[data-toggle="tooltip"]').tooltip();
});
</script>
<div class="alert alert-info">This tool helps you with selecting a new libraries (or with choosing the right library version for update) by automating a boring part of the process: It can look for known vulnerabilities.</div>
<div class="input-group">
<div id="library-identifier-wrapper">
<textarea
class="form-control" id="library-identifier"
placeholder="Specification of one library"
data-toggle="tooltip" data-placement="bottom"
title="Supported formats:<ul>@for(hint <- inputHints){<li>@hint.toString()</li>}</ul>"
data-html="true"
style="height: 46px;"
>@identifier</textarea>
</div>
<span class="input-group-btn">
<button id="submit-button" class="btn btn-primary btn-lg" onclick="LibraryAdvisorUI.scan()">Scan</button>
</span>
</div>
<div id="scan-results"></div>
}

View File

@@ -0,0 +1,35 @@
@import services.SingleLibraryScanResult
@(isDbOld: Boolean, singleLibraryScanResult: SingleLibraryScanResult)(implicit header: DefaultRequest, mainTemplateData: MainTemplateData)
@import singleLibraryScanResult.{transitiveDependencies, includesTransitive, mainDependency}
<h2>Overall result</h2>
@vulnerableTransitive = @{transitiveDependencies.exists(_.isVulnerable)}
@vulnerableMain = @{mainDependency.isVulnerable}
@if(isDbOld){
<div class="alert alert-warning">The vulnerability database seems to be outdated. Result might be thus inaccurate. Contact the administrator, please.</div>
}
@(vulnerableMain, vulnerableTransitive) match {
case (false, false) => {
<div class="alert alert-success">No vulnerability has been found in the library@if(includesTransitive){ or in its transitive dependencies}.</div>
}
case (false, true) => {<div class="alert alert-warning">While there is no vulnerability found in the library itself, but scan has identified some issues in its transitive dependencies. Maybe you should evict some dependency with a fixed version. @vulnerabilityAdvice()</div>}
case (true, false) => {<div class="alert alert-danger">There is a vulnerability found in the main dependency. Transitive dependencies are OK. Please consider using a patched version or consider impact of the vulnerabilities. @vulnerabilityAdvice()</div>}
case (true, true) => {<div class="alert alert-danger">There is a vulnerability found in both the main dependency and transitive dependencies. Please consider using a patched version or consider impact of the vulnerabilities. @vulnerabilityAdvice()</div>}
}
@if(!includesTransitive){
<div class="alert alert-warning">This type of scan does not scan transitive dependencies.</div>
}
<h2>The library itself</h2>
@dependencyList("id", Seq(mainDependency), None, expand = _.isVulnerable, addButtons = false, lazyLoad = false, showAffectedProjects = false, expandVulnerabilities = true, vulnerabilitySearch = false)
@if(includesTransitive) {
<h2>Transitive dependencies</h2>
@if(transitiveDependencies.nonEmpty) {
@if(vulnerableTransitive){
<div class="alert alert-info">Those vulnerabilities are primarily sorted by highest-rated known vulnerability. Transitive dependencies without a known vulnerability are at the end of the list.</div>
}else{
<div class="alert alert-info">There is no known vulnerability in transitive dependencies. They are listed just for your information.</div>
}
@dependencyList("id", transitiveDependencies.sorted(severityOrdering), None, expand = _.isVulnerable, addButtons = false, lazyLoad = false, showAffectedProjects = false, expandVulnerabilities = true, vulnerabilitySearch = true)
}else{
This library has no transitive dependencies.
}
}

View File

@@ -0,0 +1,4 @@
@()(implicit mainTemplateData: MainTemplateData)
@for(msg <- mainTemplateData.templateCustomization.vulnerableLibraryAdvice){
@msg
}

View File

@@ -47,6 +47,7 @@
@filter = @{projectsOption.flatMap(_._1.selectorString)}
<li><a href="@routes.Statistics.vulnerableLibraries(filter)">Vulnerable libraries</a></li>
<li><a href="@routes.Statistics.vulnerabilities(filter, None)">Vulnerabilities</a></li>
<li><a href="@routes.LibraryAdvisor.index()">Scan library</a></li>
<li><a href="@routes.Notifications.listProjects(filter)">Notifications</a></li>
<li><a href="@routes.Application.index(Map())">Status</a></li>
<li><a href="#" data-toggle="collapse" data-target=".extended-menu"></a></li>

View File

@@ -14,7 +14,6 @@
"all",
allDependencies.sortBy(_.identifiers.toIndexedSeq.sortBy(i => (i.confidence.id, i.identifierType, i.name)).mkString(", ")),
selectorOption = projectsWithSelection.selectorString,
expandByDefault = false,
addButtons = false
)

View File

@@ -62,15 +62,8 @@ $(document).ready(function(){
</div>
@dependencyList(
"vulnerable",
vulnerableDependencies.sortBy(d => (
d.maxCvssScore.map(-_), // maximum CVSS score is the king
if(d.maxCvssScore.isEmpty) Some(-d.dependencies.size) else None, // more affected dependencies if no vulnerability has defined severity
-d.vulnerabilities.size, // more vulnerabilities
-d.projects.size, // more affected projects
d.cpeIdentifiers.map(_.toCpeIdentifierOption.get).toSeq.sorted.mkString(" ")) // at least make the order deterministic
),
vulnerableDependencies.sorted(severityOrdering),
selectorOption = projectsWithSelection.selectorString,
expandByDefault = false,
addButtons = false
)
}

View File

@@ -75,7 +75,7 @@ libraryDependencies += "net.codingwell" %% "scala-guice" % "4.0.0"
libraryDependencies += "com.iheart" %% "ficus" % "1.4.0"
libraryDependencies += "org.owasp" % "dependency-check-core" % "1.4.2"
libraryDependencies += "org.owasp" % "dependency-check-core" % "1.4.5"
libraryDependencies += "com.typesafe.play" %% "play-mailer" % "3.0.1"

View File

@@ -19,6 +19,8 @@ play.i18n.langs = [ "en" ]
app{
host = "localhost" # 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 = false # Use true iff you use HTTPS
# brand = "Your brand" # optional
# vulnerableLibraryAdvice = "If in doubt, contact our security team." # optional
}
yssdc{
@@ -141,6 +143,15 @@ slick.dbs.odc {
#play.modules.disabled+="play.api.cache.EhCacheModule"
#play.cache.path = "/home/user/.cache/odc-analysis"
## [Optional] Path to OWASP Dependency Check
## Once you configure it, you enable some checking features. You also need Maven on PATH.
## (!) Note that some properties like DB credentials might be passed as arguments and thus available via /proc (depends on OS).
# odc {
# odcPath = "/path/to/dependency-check-X.Y.Z-release"
# workingDirectory = "/path/to/odc/config" # directory ODC works in; you can use relative paths from this directory
# propertyFile = "odc.props" # path to ODC property file
# extraArgs = [] # Unstable conf; This might be changed or removed without any notice!!!
# }
silhouette {
# Authenticator settings

View File

@@ -2,3 +2,4 @@ play.modules.enabled += "modules.ConfigModule"
play.modules.enabled += "modules.SilhouetteModule"
play.modules.enabled += "modules.IssueTrackerExportModule"
play.modules.enabled += "modules.EmailExportModule"
play.modules.enabled += "modules.OdcModule"

View File

@@ -33,6 +33,9 @@ GET /stats/libraries/all controllers.Statistics.allLibrarie
GET /stats/libraries/files controllers.Statistics.allFiles(selector: Option[String])
GET /stats/libraries/gavs controllers.Statistics.allGavs(selector: Option[String])
GET /advisor controllers.LibraryAdvisor.index(dependency: Option[String] ?= None)
POST /advisor/scan controllers.LibraryAdvisor.scan()
GET /notifications controllers.Notifications.listProjects(filter: Option[String])
POST /notifications/watch controllers.Notifications.watch(project: String, filter: Option[String])
POST /notifications/unwatch controllers.Notifications.unwatch(project: String, filter: Option[String])

View File

@@ -38,7 +38,7 @@ object ReportsFactory{
license = "something",
vulnerabilities = Seq(),
suppressedVulnerabilities = Seq(),
relatedDependencies = SerializableXml("<c></c>")
relatedDependencies = Seq()
)
}