admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( self::NONCE_ACTION ),
'version' => ACHWPA_VERSION,
'stages' => array( 'Bootstrap', 'MU-plugin', 'Database', 'Cron', '.htaccess', 'Users', 'REST' ),
'filesystem' => 'Filesystem',
'correlation' => 'Correlation',
'defaultBudget' => 1500,
'defaultTraversal' => 'standard',
'messages' => array(
'running' => __( 'Scanning…', 'aiutocomputerhelp-persistence-audit' ),
'complete' => __( 'Scan completed', 'aiutocomputerhelp-persistence-audit' ),
'failed' => __( 'The audit stopped because a scan request failed.', 'aiutocomputerhelp-persistence-audit' ),
'entries' => __( 'Candidate files inspected', 'aiutocomputerhelp-persistence-audit' ),
'directories' => __( 'Directories traversed', 'aiutocomputerhelp-persistence-audit' ),
'ignored' => __( 'PHP-like files skipped by metadata rules', 'aiutocomputerhelp-persistence-audit' ),
'filesSeen' => __( 'PHP-like files encountered', 'aiutocomputerhelp-persistence-audit' ),
'elapsed' => __( 'Elapsed time', 'aiutocomputerhelp-persistence-audit' ),
'stopping' => __( 'Stopping scan…', 'aiutocomputerhelp-persistence-audit' ),
'stopped' => __( 'Scan stopped by user', 'aiutocomputerhelp-persistence-audit' ),
'budget' => __( 'Budget', 'aiutocomputerhelp-persistence-audit' ),
'currentCheck' => __( 'Current check', 'aiutocomputerhelp-persistence-audit' ),
'filterFindings'=> __( 'Filter findings', 'aiutocomputerhelp-persistence-audit' ),
'traversalMode' => __( 'Traversal mode', 'aiutocomputerhelp-persistence-audit' ),
),
)
);
}
public function render_page() {
if ( ! current_user_can( self::CAPABILITY ) ) {
return;
}
?>
ajax_capability_guard();
$category = isset( $_POST['category'] ) ? sanitize_text_field( wp_unslash( $_POST['category'] ) ) : '';
$scanner = new ACHWPA_Scanner();
$allowed = array_keys( ACHWPA_Scanner::scanner_map() );
if ( ! in_array( $category, $allowed, true ) ) {
wp_send_json_error( array( 'message' => 'Invalid scan stage.' ), 400 );
}
wp_send_json_success( array( 'findings' => $this->finding_arrays( $scanner->run_stage( $category ) ) ) );
}
public function ajax_filesystem_init() {
check_ajax_referer( self::NONCE_ACTION, 'nonce' );
$this->ajax_capability_guard();
$budget = isset( $_POST['budget'] ) ? absint( $_POST['budget'] ) : 1500;
$traversal = isset( $_POST['traversal'] ) ? sanitize_key( wp_unslash( $_POST['traversal'] ) ) : 'standard';
if ( ! in_array( $traversal, array( 'standard', 'deep', 'deep-plus' ), true ) ) {
$traversal = 'standard';
}
$filesystem = new ACHWPA_Scan_Filesystem();
wp_send_json_success( array( 'state' => $filesystem->initial_state( $budget, $traversal ) ) );
}
public function ajax_filesystem_batch() {
check_ajax_referer( self::NONCE_ACTION, 'nonce' );
$this->ajax_capability_guard();
$state_json = isset( $_POST['state'] ) ? sanitize_textarea_field( wp_unslash( $_POST['state'] ) ) : '';
$state = $this->decode_json_value( $state_json );
if ( ! is_array( $state ) ) {
wp_send_json_error( array( 'message' => 'Invalid filesystem state.' ), 400 );
}
$batch_size = isset( $_POST['batch_size'] ) ? absint( $_POST['batch_size'] ) : 300;
$filesystem = new ACHWPA_Scan_Filesystem();
$batch = $filesystem->run_batch( $state, $batch_size );
$findings = $batch['findings'];
if ( ! empty( $batch['done'] ) && ! empty( $batch['state']['budget_reached'] ) ) {
$findings[] = $filesystem->budget_finding( $batch['state'] );
}
wp_send_json_success(
array(
'state' => $batch['state'],
'done' => (bool) $batch['done'],
'progress' => absint( $batch['progress'] ),
'findings' => $this->finding_arrays( $findings ),
'summary' => array(
'candidates' => isset( $batch['state']['candidates_inspected'] ) ? absint( $batch['state']['candidates_inspected'] ) : 0,
'directories' => isset( $batch['state']['directories_seen'] ) ? absint( $batch['state']['directories_seen'] ) : 0,
'files_seen' => isset( $batch['state']['files_seen'] ) ? absint( $batch['state']['files_seen'] ) : 0,
'ignored' => isset( $batch['state']['files_ignored'] ) ? absint( $batch['state']['files_ignored'] ) : 0,
),
)
);
}
public function ajax_correlate() {
check_ajax_referer( self::NONCE_ACTION, 'nonce' );
$this->ajax_capability_guard();
$findings_b64 = isset( $_POST['findings_b64'] ) ? sanitize_text_field( wp_unslash( $_POST['findings_b64'] ) ) : '';
$findings_json = base64_decode( $findings_b64, true );
$raw = is_string( $findings_json ) ? $this->decode_json_value( $findings_json ) : null;
if ( ! is_array( $raw ) ) {
wp_send_json_error( array( 'message' => 'Invalid findings payload.' ), 400 );
}
$findings = array();
foreach ( array_slice( $raw, 0, 1000 ) as $item ) {
$finding = $this->finding_from_client( $item );
if ( $finding instanceof ACHWPA_Finding ) {
$findings[] = $finding;
}
}
$scanner = new ACHWPA_Scanner();
wp_send_json_success( array( 'findings' => $this->finding_arrays( $scanner->correlate( $findings ) ) ) );
}
private function ajax_capability_guard() {
if ( ! current_user_can( self::CAPABILITY ) ) {
wp_send_json_error( array( 'message' => 'Forbidden.' ), 403 );
}
}
private function decode_json_value( $json ) {
if ( ! is_string( $json ) || '' === $json ) {
return null;
}
$decoded = json_decode( $json, true );
return is_array( $decoded ) ? $decoded : null;
}
private function finding_arrays( array $findings ) {
$out = array();
foreach ( $findings as $finding ) {
if ( $finding instanceof ACHWPA_Finding ) {
$out[] = $finding->to_array();
}
}
return $out;
}
private function finding_from_client( $item ) {
if ( ! is_array( $item ) ) {
return null;
}
$allowed_categories = array( 'Bootstrap', 'MU-plugin', 'Database', 'Cron', '.htaccess', 'Users', 'REST', 'Filesystem' );
$allowed_levels = array( 'Pass', 'Info', 'Review', 'Warning' );
$category = isset( $item['category'] ) ? sanitize_text_field( $item['category'] ) : '';
$level = isset( $item['level'] ) ? sanitize_text_field( $item['level'] ) : '';
if ( ! in_array( $category, $allowed_categories, true ) || ! in_array( $level, $allowed_levels, true ) ) {
return null;
}
return new ACHWPA_Finding(
array(
'category' => $category,
'level' => $level,
'location' => isset( $item['location'] ) ? sanitize_text_field( $item['location'] ) : '',
'indicator' => isset( $item['indicator'] ) ? sanitize_text_field( $item['indicator'] ) : '',
'explanation' => isset( $item['explanation'] ) ? sanitize_textarea_field( $item['explanation'] ) : '',
'preview' => isset( $item['preview'] ) ? sanitize_textarea_field( $item['preview'] ) : '',
'artifacts' => $this->sanitize_string_array( isset( $item['artifacts'] ) ? $item['artifacts'] : array(), 100 ),
'identifiers' => $this->sanitize_string_array( isset( $item['identifiers'] ) ? $item['identifiers'] : array(), 100 ),
'relationships' => $this->sanitize_string_array( isset( $item['relationships'] ) ? $item['relationships'] : array(), 100 ),
)
);
}
private function sanitize_string_array( $values, $limit ) {
$out = array();
foreach ( array_slice( (array) $values, 0, $limit ) as $value ) {
if ( is_scalar( $value ) ) {
$clean = sanitize_text_field( (string) $value );
if ( '' !== $clean ) {
$out[] = $clean;
}
}
}
return array_values( array_unique( $out ) );
}
}