#!/usr/bin/env bash
# ppee-policy.sh -- release gate for Windows PE build artifacts, built on ppee-cli --json.
#
#   ppee-policy.sh [options] FILE...
#     --require-signature     fail if a file has no Authenticode signature, or was modified after signing
#     --require-hardening     fail unless ASLR (DYNAMIC_BASE), DEP (NX_COMPAT) and CFG (GUARD_CF + CF table) are on
#     --forbid-wx             fail on any section that is both writable and executable
#     --forbid-admin          fail on requestedExecutionLevel=requireAdministrator in the manifest
#     --forbid-import NAME    fail if NAME (function or DLL, case-insensitive) is imported; repeatable
#     --denylist FILE         fail if a file's SHA-256 is listed in FILE (one hash per line)
#     --report FILE           also write one JSON Lines summary per input file to FILE
#
# Environment: PPEE_CLI (default: ppee-cli) -- e.g. PPEE_CLI="docker run --rm -v $PWD:$PWD:ro -w $PWD ppee-cli"
# Exit code: 0 all files pass, 1 at least one policy violation, 2 usage or analysis error.
set -uo pipefail

PPEE_CLI="${PPEE_CLI:-ppee-cli}"
SIG=0 HARD=0 WX=0 ADMIN=0 DENY="" REPORT=""
IMPORTS=()
FILES=()

while [[ $# -gt 0 ]]; do
  case "$1" in
    --require-signature) SIG=1 ;;
    --require-hardening) HARD=1 ;;
    --forbid-wx) WX=1 ;;
    --forbid-admin) ADMIN=1 ;;
    --forbid-import) IMPORTS+=("$2"); shift ;;
    --denylist) DENY="$2"; shift ;;
    --report) REPORT="$2"; shift ;;
    -h|--help) sed -n '2,15p' "$0"; exit 0 ;;
    -*) echo "unknown option: $1" >&2; exit 2 ;;
    *) FILES+=("$1") ;;
  esac
  shift
done
[[ ${#FILES[@]} -gt 0 ]] || { echo "usage: $0 [options] FILE..." >&2; exit 2; }
[[ -n "$REPORT" ]] && : > "$REPORT"

forbidden_json=$(printf '%s\n' "${IMPORTS[@]+"${IMPORTS[@]}"}" | jq -R . | jq -s 'map(select(length > 0) | ascii_downcase)')

failed=0 errors=0
for f in "${FILES[@]}"; do
  if ! json=$($PPEE_CLI --no-update-check --no-similarity --json \
        --headers --sections --hashes --imports --delay-imports --security --loadconfig --appmanifest "$f" 2>/dev/null); then
    echo "ERROR  $f: ppee-cli could not analyze the file" >&2
    errors=1
    continue
  fi

  summary=$(jq -c --argjson forbidden "$forbidden_json" '
    def hex: ascii_downcase | explode | reduce .[] as $c (0; . * 16 + (if $c >= 97 then $c - 87 else $c - 48 end));
    def bit($n): (. / $n | floor) % 2 == 1;
    (.headers["OptionalHeader.DllCharacteristics"] | hex) as $dc
    | ([.sections | to_entries[] | select(.key | endswith(".Characteristics")) | .value | hex
        | select(bit(2147483648) and bit(536870912))] | length) as $wx
    | ([.imports[], .delayImports[] | .name as $m | ($m | ascii_downcase), (.functions[] | .name // empty | ascii_downcase)]) as $imp
    | {
        path: .path,
        sha256: .fileInfo.sha256,
        signed: .security.present,
        intact: ([.security.signatures[] | .embeddedDigest == .authentihash] | all),
        signer: (.security.signatures[0].signerCertificate.subjectName // null),
        aslr: ($dc | bit(64)), dep: ($dc | bit(256)),
        cfg: (($dc | bit(16384)) and ((.loadConfig.guardCFFunction.entries // []) | length > 0)),
        wxSections: $wx,
        requireAdmin: ([.appManifest.rows[]? | select(.member | test("Level")) | .value] | any(. == "requireAdministrator")),
        forbiddenImports: [$forbidden[] as $x | select($imp | index($x)) | $x]
      }' <<<"$json")
  [[ -n "$REPORT" ]] && echo "$summary" >> "$REPORT"

  reasons=()
  j() { jq -r "$1" <<<"$summary"; }
  if (( SIG )); then
    [[ $(j .signed) == true ]] || reasons+=("not signed")
    [[ $(j .signed) == true && $(j .intact) != true ]] && reasons+=("modified after signing")
  fi
  if (( HARD )); then
    [[ $(j .aslr) == true ]] || reasons+=("ASLR off")
    [[ $(j .dep) == true ]] || reasons+=("DEP off")
    [[ $(j .cfg) == true ]] || reasons+=("CFG off")
  fi
  (( WX )) && [[ $(j .wxSections) != 0 ]] && reasons+=("$(j .wxSections) W+X section(s)")
  (( ADMIN )) && [[ $(j .requireAdmin) == true ]] && reasons+=("requireAdministrator manifest")
  bad_imports=$(j '.forbiddenImports | join(",")')
  [[ -n "$bad_imports" ]] && reasons+=("forbidden imports: $bad_imports")
  if [[ -n "$DENY" ]] && grep -qix "$(j .sha256)" "$DENY"; then reasons+=("SHA-256 on denylist"); fi

  if [[ ${#reasons[@]} -eq 0 ]]; then
    echo "PASS   $f"
  else
    echo "FAIL   $f: $(IFS=';'; echo "${reasons[*]}")"
    failed=1
  fi
done
(( errors )) && exit 2
exit $failed
