Skip to content

Instantly share code, notes, and snippets.

@unarist
Created August 1, 2024 16:22
Show Gist options
  • Save unarist/197eacba0d470b83b2df9309ac7da313 to your computer and use it in GitHub Desktop.
Save unarist/197eacba0d470b83b2df9309ac7da313 to your computer and use it in GitHub Desktop.
npm audit の結果を依存パスと共に表示するやつ
//> using scala 3.3.1
//> using toolkit default
import os.RelPath
import upickle.default.*
opaque type PackageName = String
opaque type PackageVersion = String
opaque type Severity = String
opaque type ModulePath = String
implicit def stringOrRW[T: ReadWriter]: ReadWriter[String | T] =
readwriter[ujson.Value].bimap[String | T](
{
case a: String => writeJs(a)
case b => writeJs(b)
},
{
case ujson.Str(str) => str
case json => read[T](json)
}
)
case class AuditResult(vulnerabilities: Map[PackageName, Vulnerability]) derives ReadWriter
case class Vulnerability(name: PackageName, severity: Severity, via: List[String | VulnerabilityInfo], nodes: List[ModulePath]) derives ReadWriter
case class VulnerabilityInfo(title: String, severity: Severity) derives ReadWriter
case class ListResult(dependencies: Map[PackageName, Dependency]) derives ReadWriter
case class Dependency(version: PackageVersion, dependencies: Map[PackageName, Dependency] = Map.empty) derives ReadWriter
case class PackageJson(name: PackageName, version: PackageVersion) derives ReadWriter
case class PackageNameAndVersion(name: PackageName, version: PackageVersion) {
override def toString(): String = s"$name@$version"
}
def runNpmAudit(): AuditResult = {
val auditResult = os.proc("npm", "audit", "--json").call(check = false).out.text()
read[AuditResult](auditResult)
}
def runNpmList(name: PackageName): ListResult = {
val listResult = os.proc("npm", "list", "--json", name).call().out.text()
read[ListResult](listResult)
}
def readPackageJson(modulePath: ModulePath): PackageJson = {
val packageJson = os.read(os.pwd / RelPath(modulePath) / "package.json")
read[PackageJson](packageJson)
}
def why(name: PackageName, version: String): Seq[Seq[PackageNameAndVersion]] = {
val rootDeps = runNpmList(name).dependencies
def traverse(deps: Map[PackageName, Dependency], parents: List[PackageNameAndVersion] = Nil): Seq[Seq[PackageNameAndVersion]] = {
deps.flatMap { case (depName, dep) =>
if (depName == name && dep.version == version) {
Seq(parents.reverse :+ PackageNameAndVersion(depName, dep.version))
} else {
traverse(dep.dependencies, PackageNameAndVersion(depName, dep.version) :: parents)
}
}.toSeq
}
traverse(rootDeps)
}
for (vuln <- runNpmAudit().vulnerabilities.values.filter(_.via.exists(_.isInstanceOf[VulnerabilityInfo]))) {
val lines = for {
modulePath <- vuln.nodes
packageJson = readPackageJson(modulePath)
reason <- why(packageJson.name, packageJson.version)
} yield {
val shortReason = if (reason.length > 3) reason.take(2) ++ Seq("...") ++ reason.takeRight(1) else reason
val title = vuln.via.collectFirst { case v: VulnerabilityInfo if v.severity == vuln.severity => v.title }
.orElse(vuln.via.collectFirst { case v: VulnerabilityInfo => v.title })
.getOrElse("")
val andmore = vuln.via.count(_.isInstanceOf[VulnerabilityInfo]) - 1
s"${shortReason.mkString(" :: ")}\t${vuln.severity}\t${title}${if (andmore > 0) s" (and $andmore more)" else ""}"
}
lines.distinct.foreach(println)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment