wp-rest-api/examples/run-wordpress-rest-fixture.py
Version 34da134e184e.bb2 · GPL-2.0-or-later. This preview displays packaged text and does not execute code. Treat the contents as untrusted instructions.
← Return to resource and package checksum
"""Run a disposable WordPress HTTP fixture; GPL-2.0-or-later. Never uses production mounts, networks, ports or credentials."""
import datetime as dt,hashlib,json,os,pwd,secrets,subprocess,sys,time
from pathlib import Path
root=Path(__file__).resolve().parent;stage='preflight';owned=[]
def command(args,input=None,timeout=180):
p=subprocess.run(args,input=input,text=True,capture_output=True,timeout=timeout)
if p.returncode:
if stage=='Application password revocation':
detail=[line for line in p.stderr.splitlines() if line.startswith('PHP Fatal error:') or line.startswith('PHP Warning:')];print(json.dumps({'php_failure':detail[-1][:500] if detail else 'PHP command failed without fatal-error diagnostic'}))
if stage=='HTTP probe':
for line in p.stderr.splitlines():
if line.startswith('AssertionError: {'):
detail=json.loads(line.removeprefix('AssertionError: '));print(json.dumps({'failed_case':detail['case'],'http_status':detail['status'],'response':detail['body']}))
if 'AssertionError: {' not in p.stderr:print(json.dumps({'probe_error':p.stderr.splitlines()[-1][:400] if p.stderr.splitlines() else 'No stderr'}))
raise RuntimeError('Subprocess failed: '+args[0]+' / '+stage)
return p.stdout
names={'wp':'bb-v42-rest-wp','db':'bb-v42-rest-db','network':'bb-v42-rest-net'}
for kind,name in [('container',names['wp']),('container',names['db']),('network',names['network'])]:
if subprocess.run(['docker',kind,'inspect',name],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL).returncode==0:raise RuntimeError('Fixture target already exists; refusing replacement')
try:
for tag in ['wordpress:7.1.2-php8.3-apache','mariadb:11.4']:
stage='pull '+tag
if subprocess.run(['docker','image','inspect',tag],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL).returncode==0:continue
log=root/(tag.split(':')[0]+'-pull.log')
with log.open('w') as out:p=subprocess.run(['docker','pull',tag],stdout=out,stderr=subprocess.STDOUT,text=True,timeout=600)
if p.returncode:raise RuntimeError('Official image pull failed; inspect the non-secret pull log')
images={}
for key,tag in [('wordpress','wordpress:7.1.2-php8.3-apache'),('database','mariadb:11.4'),('probe',os.environ.get('BB_REST_PROBE_IMAGE','bb-skills:20261004-v41'))]:
info=json.loads(command(['docker','image','inspect',tag]))[0];images[key]={'tag':tag,'id':info['Id'],'digests':info.get('RepoDigests',[])}
stage='network';command(['docker','network','create','--internal',names['network']]);owned.append(('network',names['network']))
db_password=secrets.token_hex(24);root_password=secrets.token_hex(24)
stage='database';command(['docker','run','-d','--name',names['db'],'--network',names['network'],'--memory','768m','--memory-swap','768m','--cpus','1','--tmpfs','/var/lib/mysql:rw,size=384m','-e','MARIADB_DATABASE=fixture','-e','MARIADB_USER=fixture','-e','MARIADB_PASSWORD='+db_password,'-e','MARIADB_ROOT_PASSWORD='+root_password,images['database']['id']]);owned.append(('container',names['db']))
for n in range(60):
p=subprocess.run(['docker','exec',names['db'],'healthcheck.sh','--connect','--innodb_initialized'],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
if p.returncode==0:break
time.sleep(1)
else:raise RuntimeError('Disposable database did not become ready')
stage='wordpress'
extra="define('WP_ENVIRONMENT_TYPE','local');define('DISABLE_WP_CRON',true);define('AUTOMATIC_UPDATER_DISABLED',true);define('WP_HTTP_BLOCK_EXTERNAL',true);define('WP_DEBUG',false);"
command(['docker','run','-d','--name',names['wp'],'--network',names['network'],'--memory','768m','--memory-swap','768m','--cpus','1','--tmpfs','/var/www/html:rw,size=256m','-e','WORDPRESS_DB_HOST='+names['db'],'-e','WORDPRESS_DB_NAME=fixture','-e','WORDPRESS_DB_USER=fixture','-e','WORDPRESS_DB_PASSWORD='+db_password,'-e','WORDPRESS_CONFIG_EXTRA='+extra,'-v',str(root)+':/fixture:ro',images['wordpress']['id']]);owned.append(('container',names['wp']))
for n in range(60):
p=subprocess.run(['docker','exec',names['wp'],'test','-f','/var/www/html/wp-config.php'],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
if p.returncode==0:break
time.sleep(1)
else:raise RuntimeError('Disposable WordPress did not initialize')
command(['docker','exec',names['wp'],'sh','-c','mkdir -p /var/www/html/wp-content/mu-plugins && cp /fixture/fixture-plugin.php /var/www/html/wp-content/mu-plugins/bb-fixture.php'])
stage='PHP syntax'
for file in ['fixture-plugin.php','setup-fixture.php']:command(['docker','exec',names['wp'],'php','-l','/fixture/'+file])
stage='WordPress setup';setup=command(['docker','exec',names['wp'],'php','/fixture/setup-fixture.php']);keys=json.loads(setup)
keys.update(json.loads(command(['docker','exec',names['wp'],'php','/fixture/setup-fixture.php','cookie'])))
assert keys['environment']['wordpress']=='7.1.2' and keys['environment']['environment_type']=='local' and keys['environment']['application_passwords_available']
stage='HTTP probe'
probe=['docker','run','--rm','-i','--network',names['network'],'--memory','192m','--cpus','1','--read-only','--tmpfs','/tmp:rw,size=16m','-v',str(root)+':/fixture:ro',images['probe']['id'],'python','/fixture/probe-rest-fixture.py']
one=json.loads(command(probe,json.dumps(keys),timeout=180))
stage='Application password revocation';revoked=json.loads(command(['docker','exec',names['wp'],'php','/fixture/setup-fixture.php','revoke']));assert revoked['revoked']==2
keys['phase']='revoked';stage='HTTP probe';two=json.loads(command(probe,json.dumps(keys),timeout=90));cases=one['cases']+two['cases'];assert len({c['name'] for c in cases})==len(cases) and all(c['status']=='passed' for c in cases)
result={'format':1,'executed_at':dt.datetime.now(dt.UTC).isoformat(),'environment':one['environment'],'probe_python':one['probe_python'],'images':images,'source_hashes':{n:hashlib.sha256((root/n).read_bytes()).hexdigest() for n in ['fixture-plugin.php','setup-fixture.php','probe-rest-fixture.py','run-wordpress-rest-fixture.py']},'scope':one['scope'],'observations':len(cases),'cases':cases,'negative_controls':1,'temporary_application_passwords_revoked':2,'internal_network':True,'published_ports':False,'production_mounts_or_networks':False,'runtime_tested':False,'native_fixture_tested':True,'external_AI_calls':False,'credentials_saved_or_exported':False}
stage='save evidence';path=root/'native-rest-evidence.json';path.write_text(json.dumps(result,indent=2));path.chmod(0o600)
print(json.dumps({'observations':len(cases),'wordpress':result['environment']['wordpress'],'php':result['environment']['php'],'native_fixture_tested':True,'runtime_tested':False,'negative_controls':1,'scope':result['scope']}))
except Exception as error:
print(json.dumps({'fixture_failed':True,'stage':stage,'error_type':type(error).__name__}));sys.exit(1)
finally:
for kind,name in reversed(owned):
args=['docker','rm','-fv',name] if kind=='container' else ['docker','network','rm',name]
subprocess.run(args,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)