Skip to content

Recipes

All recipes use --json with jq on Linux/macOS, or ConvertFrom-Json in PowerShell. Add --no-similarity to batch runs unless you want every file recorded in the similarity DB.

Reusable jq helpers

PPEE writes values from the file as hex strings (see JSON conventions), and jq has no bitwise operators. These two helpers cover both. Save them as ~/.jq, where jq loads them automatically:

~/.jq
# "C1C0" -> 49600
def hex: ascii_downcase | explode
  | reduce .[] as $c (0; . * 16 + (if $c >= 97 then $c - 87 else $c - 48 end));
# true if flag value $n is set
def bit($n): (. / $n | floor) % 2 == 1;

jq one-liners

# SHA-256 only
ppee-cli --json --hashes f.exe | jq -r .fileInfo.sha256

# Imported DLL names
ppee-cli --json --imports f.exe | jq -r '.imports[].name'

# module!function for every named import
ppee-cli --json --imports f.exe | jq -r '.imports[] | .name as $m | .functions[] | select(.name) | "\($m)!\(.name)"'

# Hardening flags from DllCharacteristics
ppee-cli --json --headers f.exe | jq '.headers["OptionalHeader.DllCharacteristics"] | hex
  | {aslr: bit(64), highEntropyVA: bit(32), nx: bit(256), cfg: bit(16384)}'

# PDB path(s)
ppee-cli --json --debug f.exe | jq -r '.debug[].codeView.pdbPath // empty'

# Resource types with their variant counts
ppee-cli --json --resources f.exe | jq -r '.resources.types[] | "\(.typeName // .name // .id)\t\([.names[].languages | length] | add)"'

# Signer subject (empty if unsigned)
ppee-cli --json --security f.exe | jq -r '.security.signatures[0].signerCertificate.subjectName // "UNSIGNED"'

# URLs found in the file
ppee-cli --json --strings f.exe | jq -r '.strings.url[].text' | sort -u

# Is it .NET?
ppee-cli --json --net f.exe | jq .net.present

Batch processing

ppee-cli takes one file per run. Use your shell or xargs/parallel to fan out; each process is independent.

hash-folder.sh: CSV of hashes for every PE under a folder
#!/usr/bin/env bash
set -uo pipefail
echo "path,sha256,imphash,ssdeep"
find "${1:-.}" -type f \( -iname '*.exe' -o -iname '*.dll' -o -iname '*.sys' \) -print0 |
  xargs -0 -P "$(nproc)" -I{} sh -c \
    'ppee-cli --no-update-check --no-similarity --json --hashes "$1" 2>/dev/null \
     | jq -r "[.path, .fileInfo.sha256, .fileInfo.impHash, .fileInfo.ssdeep] | @csv"' _ {}
Hash-Folder.ps1
Get-ChildItem -Recurse -Include *.exe,*.dll,*.sys -Path C:\Samples |
  ForEach-Object {
    $j = & ppee-cli.exe --no-update-check --no-similarity --json --hashes $_.FullName | ConvertFrom-Json
    [pscustomobject]@{ Path = $j.path; SHA256 = $j.fileInfo.sha256; ImpHash = $j.fileInfo.impHash }
  } | Export-Csv hashes.csv -NoTypeInformation
for /r C:\Samples %f in (*.exe *.dll) do ppee-cli.exe --no-update-check --json --hashes "%f" >> hashes.jsonl

Parallelism

A single ppee-cli already uses several threads for hashing and string scanning. When you run many files at once, set PPEE_JOBS=1 to avoid oversubscribing the CPU: PPEE_JOBS=1 xargs -P "$(nproc)" …

Export imports to CSV

ppee-cli --json --imports --delay-imports f.exe | jq -r '
  (.imports[]      | .name as $m | .functions[] | ["static", $m, (.name // "#\(.ordinal)")]),
  (.delayImports[] | .name as $m | .functions[] | ["delay",  $m, (.name // "#\(.ordinal)")])
  | @csv' > imports.csv

Find unsigned binaries in a folder

find dist -type f \( -name '*.exe' -o -name '*.dll' \) | while read -r f; do
  ppee-cli --no-update-check --no-similarity --json --security "$f" \
    | jq -e '.security.present' > /dev/null || echo "UNSIGNED: $f"
done

Detect tampering after signing

When the file changes after signing, the digest embedded in the signature no longer matches the file's Authentihash:

ppee-cli --json --security f.exe | jq -r '.security.signatures[]
  | if .embeddedDigest == .authentihash then "OK  \(.programName // "-")" else "MISMATCH (modified after signing)" end'

Build an IOC record

ppee-cli --json --hashes --imports --debug --richheader --no-similarity sample.exe | jq '{
  sha256: .fileInfo.sha256, md5: .fileInfo.md5, imphash: .fileInfo.impHash,
  ssdeep: .fileInfo.ssdeep, tlsh: .fileInfo.tlsh,
  pdb: [.debug[].codeView.pdbPath // empty],
  dlls: [.imports[].name],
  rich: (.richHeader.present)
}'

Compare two builds

diff <(ppee-cli --headers --sections old.exe | tail -n +2) \
     <(ppee-cli --headers --sections new.exe | tail -n +2)

tail -n +2 drops the first line, which contains the path.

Reproducible-build check

for f in a/app.exe b/app.exe; do
  ppee-cli --no-similarity --set FileHeader.TimeDateStamp=0 --set 'DataDirectory[6].Size=0' \
           --save -o "/tmp/$(basename "$(dirname "$f")").exe" "$f" > /dev/null
done
cmp /tmp/a.exe /tmp/b.exe && echo "identical apart from timestamp/debug dir"

Related: Docker batch scanning · CI policy gates · JSON reference

References

  • jq manual: the filter language used in the JSON examples on this page.