JsonReportWriter.kt
package com.depanalyzer.report
import com.depanalyzer.core.graph.DependencyNode
import com.depanalyzer.core.graph.VulnerabilityChain
internal class JsonReportWriter {
private val output = StringBuilder()
private var level = 0
fun write(report: DependencyReport): String {
obj {
field("schemaVersion", report.schemaVersion)
field("projectName", report.projectName)
field("upToDate", report.upToDate, ::writeDependencyInfo)
field("outdated", report.outdated, ::writeOutdated)
field("directVulnerable", report.directVulnerable, ::writeVulnerable)
field("transitiveVulnerable", report.transitiveVulnerable, ::writeVulnerable)
field("vulnerabilityChains", report.vulnerabilityChains, ::writeChain)
report.dependencyTree?.let { field("dependencyTree", it, ::writeTreeNode) }
report.analysis?.let { field("analysis") { writeAnalysis(it) } }
}
output.append('\n')
return output.toString()
}
private fun writeDependencyInfo(dependency: DependencyInfo) = obj {
field("groupId", dependency.groupId)
field("artifactId", dependency.artifactId)
field("version", dependency.version)
field("ecosystem", dependency.ecosystem.name)
dependency.sourceLocation?.let { field("sourceLocation") { writeLocation(it) } }
}
private fun writeOutdated(dependency: OutdatedDependency) = obj {
field("groupId", dependency.groupId)
field("artifactId", dependency.artifactId)
field("currentVersion", dependency.currentVersion)
field("latestVersion", dependency.latestVersion)
field("ecosystem", dependency.ecosystem.name)
dependency.sourceLocation?.let { field("sourceLocation") { writeLocation(it) } }
}
private fun writeVulnerable(dependency: VulnerableDependency) = obj {
field("groupId", dependency.groupId)
field("artifactId", dependency.artifactId)
field("version", dependency.version)
field("vulnerabilities", dependency.vulnerabilities, ::writeVulnerability)
dependency.dependencyChain?.let { field("dependencyChain", it, ::string) }
field("ecosystem", dependency.ecosystem.name)
dependency.sourceLocation?.let { field("sourceLocation") { writeLocation(it) } }
}
private fun writeVulnerability(vulnerability: Vulnerability) = obj {
field("cveId", vulnerability.cveId)
field("severity", vulnerability.severity.name)
vulnerability.cvssScore?.let { field("cvssScore", it) }
vulnerability.description?.let { field("description", it) }
vulnerability.advisoryId?.let { field("advisoryId", it) }
vulnerability.title?.let { field("title", it) }
if (vulnerability.cwes.isNotEmpty()) field("cwes", vulnerability.cwes, ::string)
if (vulnerability.aliases.isNotEmpty()) field("aliases", vulnerability.aliases, ::string)
field("affectedDependency") { writeAffectedDependency(vulnerability.affectedDependency) }
field("source", vulnerability.source.name)
vulnerability.retrievedAt?.let { field("retrievedAt", it.toString()) }
vulnerability.referenceUrl?.let { field("referenceUrl", it) }
}
private fun writeAffectedDependency(dependency: AffectedDependency) = obj {
field("groupId", dependency.groupId)
field("artifactId", dependency.artifactId)
field("version", dependency.version)
field("ecosystem", dependency.ecosystem.name)
}
private fun writeLocation(location: DependencySourceLocation) = obj {
field("file", location.file)
field("line", location.line)
field("startColumn", location.startColumn)
field("endColumn", location.endColumn)
}
private fun writeTreeNode(node: DependencyTreeNode) = obj {
field("groupId", node.groupId)
field("artifactId", node.artifactId)
field("currentVersion", node.currentVersion)
node.latestVersion?.let { field("latestVersion", it) }
field("isDirectDependency", node.isDirectDependency)
field("isDependencyManagement", node.isDependencyManagement)
node.scope?.let { field("scope", it) }
field("vulnerabilities", node.vulnerabilities, ::writeVulnerability)
field("children", node.children, ::writeTreeNode)
node.dependencyChain?.let { field("dependencyChain", it, ::string) }
field("ecosystem", node.ecosystem.name)
}
private fun writeChain(chain: VulnerabilityChain) = obj {
field("chain", chain.chain, ::writeDependencyNode)
field("vulnerabilities", chain.vulnerabilities, ::writeVulnerability)
field("isShortestPath", chain.isShortestPath)
field("classification", chain.classification.name)
field("depth", chain.depth)
field("cveIds", chain.cveIds, ::string)
}
private fun writeDependencyNode(node: DependencyNode) = obj {
field("id", node.id)
field("groupId", node.groupId)
field("artifactId", node.artifactId)
field("version", node.version)
node.scope?.let { field("scope", it) }
field("isDependencyManagement", node.isDependencyManagement)
field("ecosystem", node.ecosystem.name)
field("coordinate", node.coordinate)
}
private fun writeAnalysis(analysis: AnalysisMetadata) = obj {
field("requestedMode", analysis.requestedMode.name)
field("actualMode", analysis.actualMode.name)
field("projectType", analysis.projectType)
field("ecosystems", analysis.ecosystems, ::string)
field("durationMs", analysis.durationMs)
analysis.inputFingerprint?.let { field("inputFingerprint", it) }
field("vulnerabilityCoverage", analysis.vulnerabilityCoverage.name)
field("warnings", analysis.warnings, ::string)
field("providers") {
obj {
field("requested", analysis.providers.requested)
field("used", analysis.providers.used, ::string)
field("warnings", analysis.providers.warnings, ::string)
field("statuses") {
obj {
analysis.providers.statuses.forEach { (provider, status) ->
field(provider, status)
}
}
}
}
}
}
private fun obj(body: ObjectScope.() -> Unit) {
output.append('{')
level++
ObjectScope().body()
level--
newline()
output.append(indent()).append('}')
}
private fun <T> array(values: List<T>, writer: (T) -> Unit) {
output.append('[')
if (values.isNotEmpty()) {
level++
values.forEachIndexed { index, value ->
newline()
output.append(indent())
writer(value)
if (index != values.lastIndex) output.append(',')
}
level--
newline()
output.append(indent())
}
output.append(']')
}
private fun string(value: String) {
output.append('"')
value.forEach { char ->
when (char) {
'"' -> output.append("\\\"")
'\\' -> output.append("\\\\")
'\b' -> output.append("\\b")
'\u000C' -> output.append("\\f")
'\n' -> output.append("\\n")
'\r' -> output.append("\\r")
'\t' -> output.append("\\t")
else -> if (char.code < 0x20) {
output.append("\\u").append(char.code.toString(16).padStart(4, '0'))
} else {
output.append(char)
}
}
}
output.append('"')
}
private fun newline() {
output.append('\n')
}
private fun indent(): String = " ".repeat(level)
private inner class ObjectScope {
private var first = true
fun field(name: String, value: String) = field(name) { string(value) }
fun field(name: String, value: Int) = field(name) { output.append(value) }
fun field(name: String, value: Long) = field(name) { output.append(value) }
fun field(name: String, value: Double) = field(name) {
require(value.isFinite()) { "JSON does not support non-finite values" }
output.append(value)
}
fun field(name: String, value: Boolean) = field(name) { output.append(value) }
fun <T> field(name: String, values: List<T>, writer: (T) -> Unit) = field(name) {
array(values, writer)
}
fun field(name: String, writer: () -> Unit) {
if (!first) output.append(',')
newline()
output.append(indent())
string(name)
output.append(" : ")
writer()
first = false
}
}
}