"""Static PE evidence collection. Reads an image; does not patch, launch or execute it."""
import argparse, bisect, hashlib, json, pathlib, re, struct
import pefile
from capstone import Cs, CS_ARCH_X86, CS_MODE_64

p = argparse.ArgumentParser()
p.add_argument('image', type=pathlib.Path)
p.add_argument('--output', type=pathlib.Path, required=True)
p.add_argument('--target', action='append', default=[])
p.add_argument('--only-targets', action='store_true', help='Inspect only the explicitly selected native names')
p.add_argument('--rva',action='append',default=[],type=lambda value:int(value,0),help='An exact read-only resource/vtable RVA')
args = p.parse_args()
raw = args.image.read_bytes()
pe = pefile.PE(data=raw, fast_load=True)
pe.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_EXCEPTION']])
targets = ['MarketTrade', 'SetThrottle', 'SetPitch', 'SetYaw', 'SetRoll', '2.0/elite/market/trade', '2.0/elite/market/list', 'BuyCommodityFromMarketSelected', 'SellCommodityToMarketSelected', 'SupercruiseAssist', 'RequestDocking', 'Hyperspace', 'SetDestination']
targets = args.target if args.only_targets else targets + args.target
strings = {}
for rva in args.rva:strings[rva]='resource-'+hex(rva)
for text in targets:
    needle = text.encode() + b'\0'
    at = 0
    while (at := raw.find(needle, at)) >= 0:
        try: strings[pe.get_rva_from_offset(at)] = text
        except pefile.PEFormatError: pass
        at += len(needle)
ranges = sorted((e.struct.BeginAddress, e.struct.EndAddress) for e in pe.DIRECTORY_ENTRY_EXCEPTION)
starts = [x[0] for x in ranges]
refs = []
decoder = Cs(CS_ARCH_X86, CS_MODE_64)
for section in pe.sections:
    if not section.Characteristics & 0x20000000: continue
    data = section.get_data()
    for match in re.finditer(rb'[\x40-\x4f]?[\x8d\x8b][\x05\x0d\x15\x1d\x25\x2d\x35\x3d]....', data, re.S):
        rva = section.VirtualAddress + match.start()
        target = rva + len(match.group()) + struct.unpack_from('<i', match.group(), len(match.group()) - 4)[0]
        if target not in strings: continue
        index = bisect.bisect_right(starts, rva) - 1
        bounds = ranges[index] if index >= 0 and ranges[index][0] <= rva < ranges[index][1] else (rva, rva + 64)
        code = pe.get_data(bounds[0], min(8192, bounds[1] - bounds[0]))
        disassembly = [{'rva': hex(i.address), 'instruction': i.mnemonic + ' ' + i.op_str} for i in decoder.disasm(code, bounds[0])]
        refs.append({'string': strings[target], 'stringRva': hex(target), 'xrefRva': hex(rva), 'functionRva': hex(bounds[0]), 'functionEnd': hex(bounds[1]), 'instructions': disassembly})
result = {'image': args.image.name, 'sha256': hashlib.sha256(raw).hexdigest(), 'bytes': len(raw), 'imageBase': hex(pe.OPTIONAL_HEADER.ImageBase), 'strings': {hex(k):v for k,v in strings.items()}, 'references': refs, 'scope': 'Static evidence; no native invocation or signature inferred from a string alone.'}
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(result, indent=2), encoding='utf-8')
print(json.dumps({'sha256':result['sha256'], 'strings':len(strings), 'references':len(refs), 'functions':{key:list(dict.fromkeys(r['functionRva'] for r in refs if r['string']==key)) for key in targets}}, indent=2))
