diff --git a/app/com/ysoft/odc/OdcParser.scala b/app/com/ysoft/odc/OdcParser.scala index 50896b8..eaf6324 100644 --- a/app/com/ysoft/odc/OdcParser.scala +++ b/app/com/ysoft/odc/OdcParser.scala @@ -207,7 +207,7 @@ object RichBoolean{ @inline implicit def toRichBoolean(value: Boolean) = new RichBoolean(value) } -final case class Vulnerability(name: String, cweOption: Option[CWE], cvss: CvssRating, description: String, vulnerableSoftware: Seq[VulnerableSoftware], references: Seq[Reference]){ +final case class Vulnerability(name: String, /*cweOption: Option[CWE],*/ cvss: CvssRating, description: String, vulnerableSoftware: Seq[VulnerableSoftware], references: Seq[Reference]){ import RichBoolean.toRichBoolean def cvssScore = cvss.score def likelyMatchesOnlyWithoutVersion(dependencyIdentifiers: Set[Identifier]) = dependencyIdentifiers.forall { id => @@ -236,6 +236,7 @@ final case class Identifier(name: String, confidence: Confidence.Confidence, url } object OdcParser { + private val StrictMode = false private val vulnPool = new ObjectPool() private val evidencePool = new ObjectPool() @@ -251,10 +252,12 @@ object OdcParser { } def checkElements(node: Node, knownElements: Set[String]) { - val subelementNames = filterWhitespace(node).map(_.label).toSet - val unknownElements = subelementNames -- knownElements - if(unknownElements.nonEmpty){ - sys.error("Unknown elements for "+node.label+": "+unknownElements) + if(StrictMode) { + val subelementNames = filterWhitespace(node).map(_.label).toSet + val unknownElements = subelementNames -- knownElements + if (unknownElements.nonEmpty) { + sys.error("Unknown elements for " + node.label + ": " + unknownElements) + } } } @@ -264,17 +267,19 @@ object OdcParser { } def checkParams(node: Node, knownParams: Set[String]) { - val paramNames = getAttributes(node.attributes).toSet - val unknownParams = paramNames -- knownParams - if(unknownParams.nonEmpty){ - sys.error("Unknown params for "+node.label+": "+unknownParams) + if(StrictMode) { + val paramNames = getAttributes(node.attributes).toSet + val unknownParams = paramNames -- knownParams + if (unknownParams.nonEmpty) { + sys.error("Unknown params for " + node.label + ": " + unknownParams) + } } } def parseVulnerableSoftware(node: Node): VulnerableSoftware = { checkElements(node, Set("#PCDATA")) - checkParams(node, Set("allPreviousVersion")) + checkParams(node, Set("allPreviousVersion", "versionEndIncluding", "versionEndExcluding", "vulnerabilityIdMatched")) if(node.label != "software"){ sys.error(s"Unexpected element for vulnerableSoftware: ${node.label}") } @@ -298,7 +303,7 @@ object OdcParser { } def parseVulnerability(node: Node, expectedLabel: String = "vulnerability"): Vulnerability = { - checkElements(node, Set("name", "severity", "cwe", "cvssScore", "description", "references", "vulnerableSoftware", "cvssAuthenticationr", "cvssAvailabilityImpact", "cvssAccessVector", "cvssIntegrityImpact", "cvssAccessComplexity", "cvssConfidentialImpact", "notes")) + checkElements(node, Set("name", "severity", "cwe", "cwes", "cvssScore", "cvssV2", "cvssV3", "description", "references", "vulnerableSoftware", "cvssAuthenticationr", "cvssAvailabilityImpact", "cvssAccessVector", "cvssIntegrityImpact", "cvssAccessComplexity", "cvssConfidentialImpact", "notes")) // TODO: notes element is currently ignored if(node.label != expectedLabel){ sys.error(s"Unexpected element for vuln: ${node.label}") @@ -319,29 +324,79 @@ object OdcParser { } } } + + def cvssScore = { + val cvssV2 = node \ "cvssV2" + if (cvssV2.nonEmpty) { + CvssRating( + score = (cvssV2 \ "score").headOption.map(_.text.toDouble), + authenticationr = t(cvssV2 \ "authenticationr"), + availabilityImpact = t(cvssV2 \ "availabilityImpact"), + accessVector = t(cvssV2 \ "accessVector"), + integrityImpact = t(cvssV2 \ "integrityImpact"), + accessComplexity = t(cvssV2 \ "accessComplexity"), + confidentialImpact = t(cvssV2 \ "confidentialImpact") + ) + } else { + CvssRating( + score = (node \ "cvssScore").headOption.map(_.text.toDouble), + authenticationr = t(node \ "cvssAuthenticationr"), + availabilityImpact = t(node \ "cvssAvailabilityImpact"), + accessVector = t(node \ "cvssAccessVector"), + integrityImpact = t(node \ "cvssIntegrityImpact"), + accessComplexity = t(node \ "cvssAccessComplexity"), + confidentialImpact = t(node \ "cvssConfidentialImpact") + ) + } + } + vulnPool(Vulnerability( name = (node \ "name").text, //severity = (node \ "severity"), <- severity is useless, as it is computed from cvssScore :D - cweOption = (node \ "cwe").headOption.map(_.text).map(CWE.forIdentifierWithDescription), + //cweOption = (node \ "cwe").headOption.map(_.text).map(CWE.forIdentifierWithDescription), description = (node \ "description").text, - cvss = CvssRating( - score = (node \ "cvssScore").headOption.map(_.text.toDouble), - authenticationr = t(node \ "cvssAuthenticationr"), - availabilityImpact = t(node \ "cvssAvailabilityImpact"), - accessVector = t(node \ "cvssAccessVector"), - integrityImpact = t(node \ "cvssIntegrityImpact"), - accessComplexity = t(node \ "cvssAccessComplexity"), - confidentialImpact = t(node \ "cvssConfidentialImpact") - ), + cvss = cvssScore, references = (node \ "references").flatMap(filterWhitespace).map(parseReference(_)), vulnerableSoftware = (node \ "vulnerableSoftware").flatMap(filterWhitespace).map(parseVulnerableSoftware) )) } def parseIdentifier(node: Node, expectedLabel: String, parseConfidence: Boolean = true): Identifier = { - if(node.label != expectedLabel){ - sys.error("Unexpected label for identifier: "+node.label) + // Old ODC produces expectedLabel, new ODC produces package and vulnerabilityIds + node.label match { + case "suppressedIdentifier" if (node \ "id").nonEmpty => parseIdentifierNew(node, parseConfidence, matched=false) // not sure if matched + case `expectedLabel` => parseIdentifierOld(node, parseConfidence) + case "package" => parseIdentifierNew(node, parseConfidence, matched=false) + case "vulnerabilityIds" => parseIdentifierNew(node, parseConfidence, matched=true) + case "suppressedVulnerabilityIds" => parseIdentifierNew(node, parseConfidence, matched=true) + case label => sys.error(s"Expected node name package or vulnerabilityIds or $expectedLabel, got: "+label) } + } + + private val NugetPattern = """^pkg:nuget/([^@]+)@(.*)$""".r + private val MavenPattern = """^pkg:maven/([^/@]+)/([^/@]+)@(.*)$""".r + private val CpePattern = """^cpe:.*""".r + + private def parseIdentifierNew(node: Node, parseConfidence: Boolean, matched: Boolean): Identifier = { + checkElements(node, Set("id", "url", "notes")) + // TODO: process currently ignored element “notes” + checkParams(node, Set("type", "confidence")) + val id = (node \ "id").text + val (identifierType, name) = id match { + case NugetPattern(name, version) => ("nuget", s"$name:$version") + case MavenPattern(groupId, artifactId, version) => ("maven", s"$groupId:$artifactId:$version") + case CpePattern() => ("cpe", id) + case _ => ("other", id) + } + identifierPool(Identifier( + name = name, + url = (node \ "url").text, + identifierType = identifierType, + confidence = if(parseConfidence) Confidence.withName(node.attribute("confidence").get.text) else Confidence.Medium + )) + } + + private def parseIdentifierOld(node: Node, parseConfidence: Boolean): Identifier = { checkElements(node, Set("name", "url", "notes")) // TODO: process currently ignored element “notes” checkParams(node, Set("type", "confidence")) @@ -352,7 +407,7 @@ object OdcParser { case text => text // used in new ODC }, url = (node \ "url").text, - identifierType = node.attribute("type").get.text, + identifierType = try{node.attribute("type").get.text}catch{case e: NoSuchElementException => sys.error(s"No type attribute in $node")}, confidence = if(parseConfidence) Confidence.withName(node.attribute("confidence").get.text) else Confidence.Medium )) } @@ -362,7 +417,7 @@ object OdcParser { // TODO: process projectReferences checkParams(node, Set("isVirtual")) val (vulnerabilities: Seq[Node], suppressedVulnerabilities: Seq[Node]) = (node \ "vulnerabilities").headOption.map(filterWhitespace).getOrElse(Seq()).partition(_.label == "vulnerability") - val (identifiers, suppressedIdentifiers) = (node \ "identifiers").headOption.map(filterWhitespace).getOrElse(Seq()).partition(_.label == "identifier") + val (identifiers, suppressedIdentifiers) = (node \ "identifiers").headOption.map(filterWhitespace).getOrElse(Seq()).partition(!_.label.startsWith("suppressed")) dependencyPool(Dependency( fileName = (node \ "fileName").text, filePath = (node \ "filePath").text, diff --git a/app/com/ysoft/odc/statistics/LibDepStatistics.scala b/app/com/ysoft/odc/statistics/LibDepStatistics.scala index 242e316..b54abca 100644 --- a/app/com/ysoft/odc/statistics/LibDepStatistics.scala +++ b/app/com/ysoft/odc/statistics/LibDepStatistics.scala @@ -17,12 +17,12 @@ case class LibDepStatistics(libraries: Set[(Int, Library)], dependencies: Set[Gr 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 = LibDepStatistics.computeWeaknessesFrequency(vulnerabilities) + //lazy val weaknesses = vulnerabilities.flatMap(_.cweOption) + //lazy val weaknessesFrequency = LibDepStatistics.computeWeaknessesFrequency(vulnerabilities) } object LibDepStatistics{ - private def computeWeaknessesFrequency(vulnerabilities: Set[Vulnerability]) = vulnerabilities.toSeq.map(_.cweOption).groupBy(identity).mapValues(_.size).map(identity).withDefaultValue(0) + //private def computeWeaknessesFrequency(vulnerabilities: Set[Vulnerability]) = vulnerabilities.toSeq.map(_.cweOption).groupBy(identity).mapValues(_.size).map(identity).withDefaultValue(0) def apply(libraries: Set[(Int, Library)], dependencies: Set[GroupedDependency], parsedReports: Result): LibDepStatistics = LibDepStatistics( libraries = libraries, dependencies = dependencies, diff --git a/app/models/vulnerabilityOverviews.scala b/app/models/vulnerabilityOverviews.scala index 77c3042..18a47f9 100644 --- a/app/models/vulnerabilityOverviews.scala +++ b/app/models/vulnerabilityOverviews.scala @@ -11,7 +11,7 @@ abstract sealed class VulnerabilityOverview { def descriptionAttempt: String def isSureAboutDescription: Boolean def cvssScore: Option[Double] - def cweOption: Option[CWE] + //def cweOption: Option[CWE] } object VulnerabilityOverview{ @@ -23,21 +23,21 @@ final class StandardVulnerabilityOverview(vulnerability: Vulnerability) extends override def descriptionAttempt: String = vulnerability.description override def isSureAboutDescription = true override def cvssScore: Option[Double] = vulnerability.cvssScore - override def cweOption = vulnerability.cweOption + //override def cweOption = vulnerability.cweOption } private final class UnknownVulnerabilityOverview(override val name: String, link: String) extends VulnerabilityOverview { override def descriptionAttempt: String = s"Unknown vulnerability. Try looking at the following address for more details: $link" override def cvssScore: Option[Double] = None override def isSureAboutDescription = false - override def cweOption = None + //override def cweOption = None } private final class TotallyUnknownVulnerabilityOverview(override val name: String) extends VulnerabilityOverview { override def descriptionAttempt: String = s"Unknown vulnerability. Not even sure where to look for other details. Maybe Googling the identifier will help." override def cvssScore: Option[Double] = None override def isSureAboutDescription = false - override def cweOption = None + //override def cweOption = None } private object UnknownVulnerabilityOverview { diff --git a/app/services/JiraIssueTrackerService.scala b/app/services/JiraIssueTrackerService.scala index c921e5d..ab29f7a 100644 --- a/app/services/JiraIssueTrackerService.scala +++ b/app/services/JiraIssueTrackerService.scala @@ -110,7 +110,7 @@ class JiraIssueTrackerService @Inject()(absolutizer: Absolutizer, @Named("jira-s "project" -> Json.obj( "id" -> projectId.toString ), - "summary" -> s"${vulnerability.name} – ${vulnerability.cweOption.map(_ + ": ").getOrElse("")}${vulnerability.description.take(50).takeWhile(c => c != '\n' && c != '\r')}…" + "summary" -> s"${vulnerability.name} – ${vulnerability.description.take(50).takeWhile(c => c != '\n' && c != '\r')}…" ) private def extractManagedFields(vulnerability: VulnerabilityOverview, projects: Set[ReportInfo], requiresDescription: Boolean): JsObject = { @@ -121,7 +121,7 @@ class JiraIssueTrackerService @Inject()(absolutizer: Absolutizer, @Named("jira-s ) val descriptionObj = if(requiresDescription || vulnerability.isSureAboutDescription) Json.obj("description" -> extractDescription(vulnerability)) else Json.obj() val additionalFields = Seq[Option[(String, JsValueWrapper)]]( - fields.cweId.map(id => id -> vulnerability.cweOption.fold("")(_.brief)), + //fields.cweId.map(id => id -> vulnerability.cweOption.fold("")(_.brief)), fields.linkId.map(id => id -> link(vulnerability)), fields.severityId.map(id => id -> vulnerability.cvssScore), fields.projectsId.map(id => id -> projects.map(friendlyProjectNameString).toSeq.sortBy( x => (x.toLowerCase(), x)).mkString("\n")) diff --git a/app/services/OdcDbService.scala b/app/services/OdcDbService.scala index 7f3008d..3c9d211 100644 --- a/app/services/OdcDbService.scala +++ b/app/services/OdcDbService.scala @@ -48,7 +48,7 @@ class OdcDbService @Inject()(@NamedDatabase("odc") protected val dbConfigProvide } yield Some( com.ysoft.odc.Vulnerability( name = bareVuln.cve, - cweOption = bareVuln.cweOption, + //cweOption = bareVuln.cweOption, cvss = bareVuln.cvss, description = bareVuln.description, vulnerableSoftware = vulnerableSoftware, diff --git a/app/views/statistics/basic.scala.html b/app/views/statistics/basic.scala.html index b38b335..b4c8d8c 100644 --- a/app/views/statistics/basic.scala.html +++ b/app/views/statistics/basic.scala.html @@ -53,7 +53,7 @@ } -
+ @**@