mirror of
https://github.com/ysoftdevs/odc-analyzer.git
synced 2026-01-15 00:03:59 +01:00
Added support for scanning transitive dependencies for .NET libraries (except those with unlimited set of supported TMFs).
This commit is contained in:
@@ -171,7 +171,7 @@ h3.library-identification{
|
||||
vertical-align: bottom;
|
||||
width: 10px;
|
||||
}
|
||||
.dependencies-table > tbody > tr:nth-of-type(4n+1), .dependencies-table > tbody > tr:nth-of-type(4n+2) {
|
||||
.dependencies-table > tbody:hover > tr {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.dependencies-table > tbody > tr.details > td {
|
||||
|
||||
@@ -67,6 +67,7 @@ abstract sealed class AbstractDependency{
|
||||
def license: String
|
||||
def vulnerabilities: Seq[Vulnerability]
|
||||
def suppressedVulnerabilities: Seq[Vulnerability]
|
||||
def hashes = Hashes(sha1 = sha1, md5 = md5)
|
||||
}
|
||||
|
||||
final case class Dependency(
|
||||
@@ -85,7 +86,9 @@ final case class Dependency(
|
||||
isVirtual: Boolean
|
||||
) extends AbstractDependency {
|
||||
|
||||
def hashes = Hashes(sha1 = sha1, md5 = md5)
|
||||
def relatedSameDependencies: Seq[RelatedDependency] = relatedDependencies.filter(_.hashes == hashes)
|
||||
|
||||
def filePaths: Seq[String] = Seq(filePath) ++ relatedSameDependencies.map(_.filePath)
|
||||
|
||||
def plainLibraryIdentifiers: Set[PlainLibraryIdentifier] = identifiers.flatMap(_.toLibraryIdentifierOption).toSet
|
||||
|
||||
@@ -116,6 +119,7 @@ final case class RelatedDependency(
|
||||
* @param dependencies
|
||||
*/
|
||||
final case class GroupedDependency(dependencies: Map[Dependency, Set[ReportInfo]]) {
|
||||
def paths: Set[String] = dependencies.keySet.flatMap(dependency => dependency.filePaths)
|
||||
def parsedDescriptions: Seq[Seq[Seq[String]]] = descriptions.toSeq.sorted.map(_.trim.split("\n\n").filterNot(_=="").toSeq.map(_.split("\n").toSeq))
|
||||
def isVulnerable: Boolean = vulnerabilities.nonEmpty
|
||||
def maxCvssScore = (Seq(None) ++ vulnerabilities.map(_.cvssScore)).max
|
||||
|
||||
@@ -67,4 +67,12 @@ package object controllers {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts profile name (used in .NET) to a valid HTML classname.
|
||||
*/
|
||||
def profileClass(s: String): String = "profile-"+s.flatMap{
|
||||
case c if c.isLetterOrDigit && c < 128 => c.toString
|
||||
case other => s"_${other.toHexString}_"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,15 +2,12 @@ package services
|
||||
|
||||
import java.io.File.separatorChar
|
||||
import java.io._
|
||||
import java.lang.{Boolean => JBoolean}
|
||||
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, UUID, Map => JMap}
|
||||
import java.util.{Properties, UUID}
|
||||
|
||||
import _root_.org.apache.commons.lang3.SystemUtils
|
||||
import _root_.org.owasp.dependencycheck.dependency.{VulnerableSoftware => OdcVulnerableSoftware}
|
||||
import com.google.inject.Inject
|
||||
import com.ysoft.odc.{AbstractDependency, GroupedDependency, OdcParser}
|
||||
import controllers.DependencyCheckReportsParser
|
||||
@@ -21,15 +18,39 @@ import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
case class OdcDbConnectionConfig(driverClass: String, driverJar: String, url: String, user: String, password: String)
|
||||
|
||||
case class OdcConfig(odcPath: String, extraArgs: Seq[String] = Seq(), workingDirectory: String = ".", propertyFile: Option[String], cleanTmpDir: Boolean = true, dotNetNugetSource: Option[String])
|
||||
case class OdcConfig(
|
||||
odcPath: String,
|
||||
extraArgs: Seq[String] = Seq(),
|
||||
workingDirectory: String = ".",
|
||||
propertyFile: Option[String],
|
||||
cleanTmpDir: Boolean = true,
|
||||
dotNetNugetSource: Option[String],
|
||||
useDotNetCore: Boolean = false
|
||||
)
|
||||
|
||||
case class SingleLibraryScanResult(mainDependencies: Seq[GroupedDependency], transitiveDependencies: Seq[GroupedDependency], includesTransitive: Boolean, limitations: Seq[String]) {
|
||||
abstract sealed class Limitation(val severity: String){
|
||||
def message: String
|
||||
def requiresAttention: Boolean
|
||||
}
|
||||
|
||||
object Limitation{
|
||||
case class Notice(message: String) extends Limitation("info"){
|
||||
override def requiresAttention: Boolean = false
|
||||
}
|
||||
case class Warning(message: String) extends Limitation("warning"){
|
||||
override def requiresAttention: Boolean = true
|
||||
}
|
||||
}
|
||||
|
||||
case class PreparationResult(limitations: Seq[Limitation] = Seq(), profilesOption: Option[(Seq[String], GroupedDependency => Seq[String])] = None, includesTransitive: Boolean)
|
||||
|
||||
case class SingleLibraryScanResult(mainDependencies: Seq[GroupedDependency], transitiveDependencies: Seq[GroupedDependency], includesTransitive: Boolean, limitations: Seq[Limitation], profilesOption: Option[(Seq[String], GroupedDependency => Seq[String])]) {
|
||||
def allDependencies: Seq[GroupedDependency] = mainDependencies ++ transitiveDependencies
|
||||
}
|
||||
|
||||
class OdcInstallation(val workingDirectory: Path, odcPath: Path){
|
||||
private def suffix = if(SystemUtils.IS_OS_WINDOWS) "bat" else "sh"
|
||||
def odcBin = odcPath.resolve("bin").resolve("dependency-check."+suffix).toFile.getAbsolutePath
|
||||
def odcBin: String = odcPath.resolve("bin").resolve("dependency-check."+suffix).toFile.getAbsolutePath
|
||||
def odcVersion: String = {
|
||||
import sys.process._
|
||||
Seq(odcBin, "--version").!!.trim.reverse.takeWhile(_!=' ').reverse
|
||||
@@ -72,13 +93,13 @@ class OdcService @Inject() (odcConfig: OdcConfig, odcDbConnectionConfig: OdcDbCo
|
||||
val hasUnknownWebJar = hasMavenIdentifier(isUnknownWebJarIdentifier)
|
||||
val hasUnrecommendedWebJar = hasManualWebJar || hasBowerWebJar || hasUnknownWebJar
|
||||
val additionalLimitations = if(hasUnrecommendedWebJar)
|
||||
Seq(
|
||||
Seq(Limitation.Warning(
|
||||
"You seem to use some WebJar other than NPM. Please consider using a NPM variant of the WebJar if possible. "+
|
||||
"NPM has currently the best support and ODC is most likely to find vulnerabilities (if they are present) there."+
|
||||
(if(hasBowerWebJar) " Bower is deprecated." else "")+
|
||||
(if(hasManualWebJar) " Classic WebJars require manual work of maintainer, so they might be harder to update." else "")+
|
||||
(if(hasUnknownWebJar) " You seem to use some kind of WebJar this tool does not know (NPM/Bower/Classic)." else "")
|
||||
) else Seq()
|
||||
)) else Seq()
|
||||
result.copy(limitations = result.limitations ++ additionalLimitations)
|
||||
}
|
||||
|
||||
@@ -130,22 +151,13 @@ class OdcService @Inject() (odcConfig: OdcConfig, odcDbConnectionConfig: OdcDbCo
|
||||
</dependencies>
|
||||
</project>
|
||||
Files.write(dir.resolve("pom.xml"), pomXml.toString.getBytes(UTF_8))
|
||||
PreparationResult(includesTransitive = true)
|
||||
}.map(addMavenLibsLimitations)
|
||||
|
||||
def scanDotNet(packageName: String, version: String): Future[SingleLibraryScanResult] = scanInternal(
|
||||
createOdcCommand = createStandardOdcCommand,
|
||||
isMainLibraryOption = Some(dep =>
|
||||
(dep.fileName == s"$packageName.dll") ||
|
||||
(dep.fileName == s"$packageName.$version.nupkg") ||
|
||||
(dep.fileName == s"$packageName.$version.nupkg: $packageName.nuspec")
|
||||
),
|
||||
enableMultipleMainLibraries = true,
|
||||
limitations = Seq("Scans for .NET libraries usually contain multiple DLL variants of the same library, because multiple targets (e.g., .NETFramework 4.0, .NETFramework 4.5, .NETStandard 1.0, Portable Class Library, …) are scanned.")
|
||||
){(odcInstallation, dir) =>
|
||||
private def nugetRestore(odcInstallation: OdcInstallation, dir: Path, packagesConfigFile: Path, packageName: String, version: String): Unit = {
|
||||
val packagesConfig = <packages>
|
||||
<package id={packageName} version={version} />
|
||||
</packages>
|
||||
val packagesConfigFile = dir.resolve("..").resolve("packages.config")
|
||||
<package id={packageName} version={version}/>
|
||||
</packages>
|
||||
Files.write(packagesConfigFile, packagesConfig.toString().getBytes(UTF_8))
|
||||
val cmd = Seq(
|
||||
nugetBin,
|
||||
@@ -160,7 +172,7 @@ class OdcService @Inject() (odcConfig: OdcConfig, odcDbConnectionConfig: OdcDbCo
|
||||
start()
|
||||
val rawLog = consumeStream(process.getInputStream)
|
||||
val res = process.waitFor()
|
||||
if(res != 0){
|
||||
if (res != 0) {
|
||||
val log = new String(rawLog)
|
||||
val NotFoundRegex = """Unable to find version '([^']+)' of package '([^']+)'.""".r
|
||||
log.lines.toStream.head match {
|
||||
@@ -170,6 +182,93 @@ class OdcService @Inject() (odcConfig: OdcConfig, odcDbConnectionConfig: OdcDbCo
|
||||
}
|
||||
}
|
||||
|
||||
private def dotnetRestore(odcInstallation: OdcInstallation, dir: Path, csprojFile: Path, packageName: String, version: String, targetFramework: String): Unit = {
|
||||
val csproj = <Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>{targetFramework}</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include={packageName} Version={version} />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Files.write(csprojFile, csproj.toString().getBytes(UTF_8))
|
||||
val cmd = Seq(
|
||||
"dotnet",
|
||||
"restore",
|
||||
csprojFile.toString,
|
||||
"--packages",
|
||||
dir.toString
|
||||
) ++ odcConfig.dotNetNugetSource.fold(Seq[String]())(source => Seq("--source", source))
|
||||
val process = new ProcessBuilder(cmd: _*).
|
||||
directory(odcInstallation.workingDirectory.toFile).
|
||||
redirectErrorStream(true).
|
||||
start()
|
||||
val rawLog = consumeStream(process.getInputStream)
|
||||
val res = process.waitFor()
|
||||
if(res != 0){
|
||||
val log = new String(rawLog) // we probably should use the default encoding when it comes from a process through a pipe…
|
||||
sys.error(s"Bad return code from DotNet restore: $res. Output: $log")
|
||||
}
|
||||
}
|
||||
|
||||
def findDotNetProfiles(dir: Path)(gd: GroupedDependency): Seq[String] = {
|
||||
// each .NET framework profile has a separate directory starting with "framework-". We just parse that to get profile from path.
|
||||
val pathPrefix = dir.toString + File.separatorChar
|
||||
gd.paths.toSeq.map{path =>
|
||||
if(path startsWith pathPrefix){
|
||||
path.substring(pathPrefix.length).takeWhile(_ != File.separatorChar)
|
||||
}else{
|
||||
sys.error(s"Unexpected path: $path")
|
||||
}
|
||||
}.collect{
|
||||
case s if s startsWith "framework-" => s.substring("framework-".length)
|
||||
}.distinct
|
||||
}
|
||||
|
||||
def scanDotNet(packageName: String, version: String): Future[SingleLibraryScanResult] = scanInternal(
|
||||
createOdcCommand = createStandardOdcCommand,
|
||||
isMainLibraryOption = Some(dep => {
|
||||
val fileNameCanon = dep.fileName.toLowerCase()
|
||||
(fileNameCanon == s"$packageName.dll".toLowerCase()) ||
|
||||
(fileNameCanon == s"$packageName.$version.nupkg".toLowerCase()) ||
|
||||
(fileNameCanon == s"$packageName:$version".toLowerCase()) ||
|
||||
(fileNameCanon == s"$packageName.$version.nupkg: $packageName.nuspec".toLowerCase())
|
||||
}
|
||||
),
|
||||
enableMultipleMainLibraries = true,
|
||||
limitations = Seq(Limitation.Notice("Scans for .NET libraries usually contain multiple DLL variants of the same library, because multiple targets (e.g., .NETFramework 4.0, .NETFramework 4.5, .NETStandard 1.0, Portable Class Library, …) are scanned."))
|
||||
){(odcInstallation, dir) =>
|
||||
import scala.collection.JavaConverters._
|
||||
val packagesConfigFile = dir.resolve("packages.config")
|
||||
val plainDir = dir.resolve("plain")
|
||||
Files.createDirectory(plainDir)
|
||||
nugetRestore(odcInstallation, plainDir, packagesConfigFile, packageName, version)
|
||||
if(odcConfig.useDotNetCore) {
|
||||
val libDir = Files.list(plainDir).iterator().asScala.toIndexedSeq match {
|
||||
case Seq(single) => single.resolve("lib")
|
||||
case Seq() => sys.error("missing directory after resolution")
|
||||
case other => sys.error(s"Seems like some unexpected files: $other")
|
||||
}
|
||||
if (Files.exists(libDir)) {
|
||||
val profiles = Files.list(libDir).iterator().asScala.toIndexedSeq.map(_.getFileName.toString)
|
||||
for (targetFramework <- profiles) {
|
||||
val csprojFile = dir.resolve("ad-hoc-project-" + targetFramework + ".csproj")
|
||||
val tfDir = dir.resolve("framework-" + targetFramework)
|
||||
Files.createDirectory(tfDir)
|
||||
dotnetRestore(odcInstallation, tfDir, csprojFile, packageName, version, targetFramework)
|
||||
}
|
||||
PreparationResult(profilesOption = Some((profiles, findDotNetProfiles(dir))), includesTransitive = true)
|
||||
} else {
|
||||
// In this case, we don't have a set of TFMs for scanning. We would have to resolve dependencies for all of them.
|
||||
// We cannot pick just one of them until we are sure that some of dependencies cannot have some other TMF-dependent dependencies.
|
||||
PreparationResult(limitations = Seq(Limitation.Warning("Transitive dependencies are not scanned, because it is not supported for .NET libraries without a limited set of target frameworks.")), includesTransitive = false)
|
||||
}
|
||||
}else{
|
||||
// fallback to old mode without transitive dependencies
|
||||
PreparationResult(includesTransitive = false, limitations = Seq(Limitation.Warning("Transitive dependencies are not scanned, because odc.useDotNetCore is not enabled. See config.")))
|
||||
}
|
||||
}
|
||||
|
||||
private def consumeStream(in: InputStream): Array[Byte] = {
|
||||
val baos = new ByteArrayOutputStream()
|
||||
val buff = new Array[Byte](1024)
|
||||
@@ -185,9 +284,9 @@ class OdcService @Inject() (odcConfig: OdcConfig, odcDbConnectionConfig: OdcDbCo
|
||||
isMainLibraryOption: Option[AbstractDependency => Boolean],
|
||||
logChecks: String => Unit = s => (),
|
||||
enableMultipleMainLibraries: Boolean = false,
|
||||
limitations: Seq[String] = Seq.empty
|
||||
limitations: Seq[Limitation] = Seq.empty
|
||||
)(
|
||||
f: (OdcInstallation, Path) => Unit
|
||||
f: (OdcInstallation, Path) => PreparationResult
|
||||
): Future[SingleLibraryScanResult] = Future{
|
||||
withTmpDir { scanDir =>
|
||||
val odcInstallation = resolveOdcInstallation
|
||||
@@ -195,7 +294,7 @@ class OdcService @Inject() (odcConfig: OdcConfig, odcDbConnectionConfig: OdcDbCo
|
||||
val reportFilename = s"${scandirPrefix}report.xml"
|
||||
val path = scanDir.resolve("scanned-dir")
|
||||
Files.createDirectory(path)
|
||||
f(odcInstallation, path)
|
||||
val preparationResult = f(odcInstallation, path)
|
||||
val cmd: Seq[String] = createOdcCommand(odcInstallation, scandirPrefix, path, reportFilename)
|
||||
val process = new ProcessBuilder(cmd: _*).
|
||||
directory(odcInstallation.workingDirectory.toFile).
|
||||
@@ -211,29 +310,24 @@ class OdcService @Inject() (odcConfig: OdcConfig, odcDbConnectionConfig: OdcDbCo
|
||||
sys.error(s"Non-zero return value: $res; output: $log")
|
||||
}
|
||||
val result = DependencyCheckReportsParser.forAdHocScan(OdcParser.parseXmlReport(Files.readAllBytes(Paths.get(reportFilename))))
|
||||
result.allDependencies.partition{case (dep, _) =>
|
||||
val (mainLibraries, otherLibraries) = result.allDependencies.partition{case (dep, _) =>
|
||||
isMainLibraryOption.fold(true)(f => f(dep) || dep.relatedDependencies.exists(f))
|
||||
} match {
|
||||
case (Seq(), _) => sys.error("No library is selected as the main library")
|
||||
case (Seq(mainLibrary), otherLibraries) =>
|
||||
SingleLibraryScanResult(
|
||||
mainDependencies = Seq(GroupedDependency(Seq(mainLibrary))),
|
||||
transitiveDependencies = otherLibraries.map(dep => GroupedDependency(Seq(dep))),
|
||||
includesTransitive = isMainLibraryOption.isDefined,
|
||||
limitations = limitations
|
||||
)
|
||||
case (mainLibraries, otherLibraries) =>
|
||||
if(enableMultipleMainLibraries) {
|
||||
SingleLibraryScanResult(
|
||||
mainDependencies = mainLibraries.map(dep => GroupedDependency(Seq(dep))),
|
||||
transitiveDependencies = otherLibraries.map(dep => GroupedDependency(Seq(dep))),
|
||||
includesTransitive = isMainLibraryOption.isDefined,
|
||||
limitations = limitations
|
||||
)
|
||||
} else {
|
||||
sys.error(s"multiple (${mainLibraries.size}) libraries selected as the main library: "+otherLibraries)
|
||||
}
|
||||
}
|
||||
mainLibraries.size match {
|
||||
case 0 => sys.error("No library is selected as the main library")
|
||||
case 1 => // that's OK
|
||||
case _ if enableMultipleMainLibraries => // that's OK
|
||||
case _ if !enableMultipleMainLibraries => sys.error(s"multiple (${mainLibraries.size}) libraries selected as the main library: "+mainLibraries)
|
||||
}
|
||||
val mainDependencies = mainLibraries.map(dep => GroupedDependency(Seq(dep)))
|
||||
val transitiveDependencies = otherLibraries.map(dep => GroupedDependency(Seq(dep)))
|
||||
SingleLibraryScanResult(
|
||||
mainDependencies = mainDependencies,
|
||||
transitiveDependencies = transitiveDependencies,
|
||||
includesTransitive = preparationResult.includesTransitive,
|
||||
limitations = limitations ++ preparationResult.limitations,
|
||||
profilesOption = preparationResult.profilesOption
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@(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)
|
||||
@(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, profilesOption: Option[(Seq[String], GroupedDependency => Seq[String])] = None)
|
||||
@cpeHtmlId(cpe: String) = @{
|
||||
cpe.getBytes("utf-8").mkString("-")
|
||||
}
|
||||
|
||||
<table class="table table-condensed dependencies-table">
|
||||
<table class="table table-condensed dependencies-table" id="@idPrefix-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Severity</th>
|
||||
@@ -12,37 +12,39 @@
|
||||
<th class="actions"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@for(dep <- list; depPrefix = s"$idPrefix-${dep.hashes.serialized}"){
|
||||
<tr>
|
||||
<td class="severity">
|
||||
@dep.maxCvssScore.fold{
|
||||
<span class="label label-success">OK</span>
|
||||
}{ s =>
|
||||
<span class="score-vulnerability">@s</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>
|
||||
@for(dep <- list; depPrefix = s"$idPrefix-${dep.hashes.serialized}"; classes={profilesOption.fold(Seq[String]()){case (_, parser) => parser(dep).map(profileClass)}}){
|
||||
<tbody class="@((classes++Seq("library")).mkString(" "))">
|
||||
<tr>
|
||||
<td class="severity">
|
||||
@dep.maxCvssScore.fold{
|
||||
<span class="label label-success">OK</span>
|
||||
}{ s =>
|
||||
<span class="score-vulnerability">@s</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">
|
||||
@libraryIdentificationList(dep, Some(cpe => s"$idPrefix-${dep.hashes.serialized}-suppression-cpe-${cpeHtmlId(cpe)}"), addLink = false, addButtons = addButtons)
|
||||
</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 @if(!expand(dep)){collapsed} expandable expandable-right"></button>
|
||||
</td>
|
||||
</tr>
|
||||
<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
|
||||
)"}>
|
||||
@if(!lazyLoad){
|
||||
<td colspan="4">@dependencyDetailsInner(depPrefix = depPrefix, dep = dep, selectorOption = selectorOption, showAffectedProjects = showAffectedProjects, expandVulnerabilities = expandVulnerabilities, vulnerabilitySearch = vulnerabilitySearch)</td>
|
||||
}
|
||||
</td>
|
||||
<td class="identifiers">
|
||||
@libraryIdentificationList(dep, Some(cpe => s"$idPrefix-${dep.hashes.serialized}-suppression-cpe-${cpeHtmlId(cpe)}"), addLink = false, addButtons = addButtons)
|
||||
</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 @if(!expand(dep)){collapsed} expandable expandable-right"></button>
|
||||
</td>
|
||||
</tr>
|
||||
<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
|
||||
)"}>
|
||||
@if(!lazyLoad){
|
||||
<td colspan="4">@dependencyDetailsInner(depPrefix = depPrefix, dep = dep, selectorOption = selectorOption, showAffectedProjects = showAffectedProjects, expandVulnerabilities = expandVulnerabilities, vulnerabilitySearch = vulnerabilitySearch)</td>
|
||||
}
|
||||
</tr>
|
||||
</tr>
|
||||
</tbody>
|
||||
}
|
||||
</table>
|
||||
<script type="text/javascript">
|
||||
|
||||
@@ -45,6 +45,22 @@
|
||||
enableSubmit();
|
||||
}
|
||||
});
|
||||
},
|
||||
filterByProfile: function (el){
|
||||
$("> *", el.parentNode).removeClass("active");
|
||||
function filter(root){
|
||||
var profileClass = $(el).data("profileclass");
|
||||
var allLibraries = $(root.getElementsByClassName("library"));
|
||||
if(profileClass){
|
||||
allLibraries.hide();
|
||||
$(root.getElementsByClassName(profileClass)).show();
|
||||
}else{
|
||||
allLibraries.show();
|
||||
}
|
||||
}
|
||||
filter(document.getElementById("main-table"));
|
||||
filter(document.getElementById("transitive-table"));
|
||||
$(el).addClass("active");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,28 +1,43 @@
|
||||
@import services.SingleLibraryScanResult
|
||||
@(isDbOld: Boolean, singleLibraryScanResult: SingleLibraryScanResult)(implicit header: DefaultRequest, mainTemplateData: MainTemplateData)
|
||||
@import singleLibraryScanResult.{transitiveDependencies, includesTransitive, mainDependencies, limitations}
|
||||
@import singleLibraryScanResult.{transitiveDependencies, includesTransitive, mainDependencies, limitations, profilesOption}
|
||||
@requiresAttention = @{limitations.exists(_.requiresAttention)}
|
||||
@for((profiles, _) <- profilesOption){
|
||||
<h2>Profiles</h2>
|
||||
@if(profiles.size > 1) {
|
||||
<p>This scan provider results for multiple profiles. By default, all results all shown, but you can filter it.</p>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-default" onclick="LibraryAdvisorUI.filterByProfile(this)">All profiles</button>
|
||||
@for(profile <- profiles) {
|
||||
<button type="button" class="btn btn-default" onclick="LibraryAdvisorUI.filterByProfile(this)" data-profileclass="@profileClass(profile)">@profile</button>
|
||||
}
|
||||
</div>
|
||||
}else{
|
||||
All results belong to profile <strong>@profiles.head</strong>.
|
||||
}
|
||||
}
|
||||
<h2>Overall result</h2>
|
||||
@vulnerableTransitive = @{transitiveDependencies.exists(_.isVulnerable)}
|
||||
@vulnerableMain = @{mainDependencies.exists(_.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>
|
||||
}
|
||||
@for(limitations <- limitations){
|
||||
<div class="alert alert-warning"><strong>This scan has some limitations: </strong>@limitations</div>
|
||||
@for(limitation <- limitations){
|
||||
<div class="alert alert-@limitation.severity"><strong>Limitation: </strong>@limitation.message</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>
|
||||
<div class="alert alert-@if(requiresAttention){warning}else{success}">
|
||||
No vulnerability has been found in the library@if(includesTransitive){ or in its transitive dependencies}.
|
||||
@if(requiresAttention){However, take care of the limitations above, please.}
|
||||
</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", mainDependencies, None, expand = _.isVulnerable, addButtons = false, lazyLoad = false, showAffectedProjects = false, expandVulnerabilities = true, vulnerabilitySearch = false)
|
||||
@dependencyList("main", mainDependencies, None, expand = _.isVulnerable, addButtons = false, lazyLoad = false, showAffectedProjects = false, expandVulnerabilities = true, vulnerabilitySearch = false, profilesOption = profilesOption)
|
||||
@if(includesTransitive) {
|
||||
<h2>Transitive dependencies</h2>
|
||||
@if(transitiveDependencies.nonEmpty) {
|
||||
@@ -31,7 +46,7 @@
|
||||
}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)
|
||||
@dependencyList("transitive", transitiveDependencies.sorted(severityOrdering), None, expand = _.isVulnerable, addButtons = false, lazyLoad = false, showAffectedProjects = false, expandVulnerabilities = true, vulnerabilitySearch = true, profilesOption = profilesOption)
|
||||
}else{
|
||||
This library has no transitive dependencies.
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ slick.dbs.odc {
|
||||
# dotNetNugetSource = "https://path/to/your/nuget/proxy" # [optional]
|
||||
# extraArgs = [] # [optional] Unstable conf; This might be changed or removed without any notice!!!
|
||||
# cleanTmpDir = true # [optional] Keep temporary directory content for debugging
|
||||
# useDotNetCore = true # [optional] Use .NET Core for library scanning. You need dotnet executable on $PATH. This is currently useful for scanning of transitive dependencies.
|
||||
# }
|
||||
|
||||
silhouette {
|
||||
|
||||
Reference in New Issue
Block a user