Nanyx

Malicious hash lookup

Check package hashes against an API and write the malicious ones to CSV.

module virusTotalChecker

import (
  nanyx/io
  nanyx/encoding
  nanyx.web/http
  nanyx.web/json
)

-- Look up a list of package hashes against the VirusTotal API and write the
-- malicious ones to a CSV file.

def apiKey = "aisrtearst"

def shas = [
  "KXZRDb+xqPFWSygCD1lVA5lFfuUBTJ+P8V2Akz6f+50="
  "dKyyT0hWDWXKicr8OajeZ2olfeVTo+IvJOIuNpirYqg="
  "zgUyYC8TP9i5NJWD5tGFwJfVdjordW5lWLvX7SICdWI="
  "R9GtGzxw9zQ2bxZ51BIXk4EHnQXZoyMwOe9Uo+CJKw8="
  "dmxPnjzTulEP9g/h0oWDefcUGdQG6tlkOp2XFT0oug4="
  "c0VWBFHoddevVkOxwGakMYn8RXq8RkkFyZLSojl/VWI="
]

type Verdict = (
  name: string
  sha256base64: string
  sha256hex: string
  status: #ok | #malicious
  maliciousVotes: int
)

type LookupError = #httpError(int) | #badResponse

-- VirusTotal identifies files by hex-encoded SHA-256
def toHex: string -> string = { base64 ->
  base64 \trim \Base64.decode \Hex.encode \toLower
}

type Report = (names: list(string), malicious: int)

def reportDecoder = Json.record(
  names = Json.path(["data", "attributes", "names"], Json.list(Json.string))
  malicious = Json.path(["data", "attributes", "last_analysis_stats", "malicious"], Json.int)
)

def checkOk: Response -> #some(Response) | #error(#httpError(int)) = { response ->
  if response.ok then #some(response) else #error(#httpError(response.status))
}

def lookup: [Http] string -> #some(Verdict) | #error(LookupError) = { sha -> proc {
  def hex = toHex(sha)

  def response = try Http.get(
    url = "https://www.virustotal.com/api/v3/files/{hex}"
    headers = ["x-apikey" => apiKey]
  ) \Result.then(checkOk)

  def report = try Json.decode(response.body, reportDecoder) \Result.mapError { _ -> #badResponse }
  def #some(name) = report.names \head except #emptyList -> #some("<unknown>")

  return Verdict(
    name = name
    sha256base64 = sha
    sha256hex = hex
    status = if report.malicious == 0 then #ok else #malicious
    maliciousVotes = report.malicious
  )
}}

def toCsvLine: Verdict -> string = { v ->
  [v.name, v.sha256base64, v.sha256hex, "{v.maliciousVotes}"] \String.join(",")
}

@entry
def main: [Console & Http & FileSystem & Clock] () -> () = {
  def verdicts = shas \map(lookup)

  for verdicts | verdict ->
    match verdict
      | #some(v) -> println("Retrieved {v.sha256hex} ({v.name}): {v.status}")
      | #error(#httpError(status)) -> println("HTTP error {status}")
      | #error(#badResponse) -> println("Unreadable response")

  def malicious = verdicts \Option.values \filter { .status == #malicious }
  def fileName = Clock.now \format("yyyy-MM-dd_HH-mm-ss") ++ ".csv"

  File.writeLines(fileName, malicious \map(toCsvLine))
}

This program lives in the repository as examples/malicious-shas.nyx.