namespace = 'agenticdaisy-firewall/v1'; $this->rest_base = 'scan'; } /** * Register routes. */ public function register_routes(): void { register_rest_route( 'agenticdaisy-firewall/v1', '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * The report names every credential and every privileged account on the * site, so reading it is administrator work - the same bar as every other * route in this namespace. * * @param WP_REST_Request> $request Request. * @return true|\WP_Error */ public function get_item_permissions_check( $request ) { unset( $request ); if ( current_user_can( 'manage_options' ) ) { return true; } return new \WP_Error( 'rest_forbidden', __( 'You are not allowed to run an exposure scan on this site.', 'agentic-daisy-ai-agent-firewall' ), array( 'status' => rest_authorization_required_code() ) ); } /** * Run the scan. * * @param WP_REST_Request> $request Request. */ public function get_item( $request ): WP_REST_Response { unset( $request ); $report = $this->runner->run(); $generated_at = gmdate( 'c' ); // Read before writing: "when was this site last looked at" is the // question an agency has about a site it inherited, and it is only // answerable while the previous value is still there. $previous = get_option( self::OPTION_LAST_RUN ); update_option( self::OPTION_LAST_RUN, $generated_at, false ); return new WP_REST_Response( array( 'generated_at' => $generated_at, 'last_run_at' => is_string( $previous ) && '' !== $previous ? $previous : null, 'clear' => $report->is_clear(), 'counts' => $report->counts(), 'checks' => array_map( array( $this, 'prepare_check' ), $report->results ), ), 200 ); } /** * One check's section of the report. * * @param CheckResult $result Check result. * @return array */ private function prepare_check( CheckResult $result ): array { return array( 'id' => $result->id, 'label' => $result->label, 'error' => $result->error, 'findings' => array_map( array( $this, 'prepare_finding' ), $result->findings ), ); } /** * One finding. * * @param Finding $finding Finding. * @return array */ private function prepare_finding( Finding $finding ): array { return array( 'severity' => $finding->severity->value, 'title' => $finding->title, 'detail' => $finding->detail, 'subject' => $finding->subject, 'tab' => $finding->tab, ); } /** * Response schema. * * @return array */ public function get_item_schema(): array { $severities = array_map( static fn( Severity $s ): string => $s->value, Severity::cases() ); return array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'adaf_scan_report', 'type' => 'object', 'properties' => array( 'generated_at' => array( 'description' => 'When this scan ran, ISO 8601 in UTC.', 'type' => 'string', 'readonly' => true, ), 'last_run_at' => array( 'description' => 'When the scan last ran before this one; null if it never had.', 'type' => array( 'string', 'null' ), 'readonly' => true, ), 'clear' => array( 'description' => 'True when every check ran and none had anything to report.', 'type' => 'boolean', 'readonly' => true, ), 'counts' => array( 'description' => 'Findings per severity.', 'type' => 'object', 'additionalProperties' => array( 'type' => 'integer' ), 'readonly' => true, ), 'checks' => array( 'description' => 'One entry per check, in report order.', 'type' => 'array', 'readonly' => true, 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'type' => 'string' ), 'label' => array( 'type' => 'string' ), 'error' => array( 'type' => array( 'string', 'null' ) ), 'findings' => array( 'type' => 'array', 'items' => array( 'type' => 'object', 'properties' => array( 'severity' => array( 'type' => 'string', 'enum' => $severities, ), 'title' => array( 'type' => 'string' ), 'detail' => array( 'type' => 'string' ), 'subject' => array( 'type' => 'string' ), 'tab' => array( 'type' => array( 'string', 'null' ) ), ), ), ), ), ), ), ), ); } }