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.