'array', 'sanitize_callback' => 'cpradar_sanitize', ) ); if ( function_exists( 'wp_add_privacy_policy_content' ) ) { wp_add_privacy_policy_content( __( 'SyncNet Patch Radar', 'syncnet-patch-radar' ), wp_kses_post( wpautop( __( 'Our security alerts page is compiled automatically from public data of the German BSI, the US agency CISA and the US National Vulnerability Database (NVD). Our server retrieves this data; visitors\' browsers do not connect to these services. The page sets no cookies and does not track visitors. Share buttons are plain links: only when you click LinkedIn, WhatsApp or Teams does the respective provider\'s page open and process data according to its own privacy policy.', 'syncnet-patch-radar' ) ) ) ); } } ); /** * Sanitize settings. * * @param array $in Raw input. * @return array */ function cpradar_sanitize( $in ) { $old = cpradar_settings(); $in = is_array( $in ) ? $in : array(); $out = array( 'enabled' => empty( $in['enabled'] ) ? 0 : 1, 'bsi' => empty( $in['bsi'] ) ? 0 : 1, 'cisa' => empty( $in['cisa'] ) ? 0 : 1, 'nvd' => empty( $in['nvd'] ) ? 0 : 1, 'cvss' => max( 7.0, min( 10.0, round( (float) str_replace( ',', '.', (string) ( $in['cvss'] ?? 9 ) ), 1 ) ) ), 'days' => max( 7, min( 365, (int) ( $in['days'] ?? 60 ) ) ), 'fresh' => max( 0, min( 90, (int) ( $in['fresh'] ?? 14 ) ) ), 'menu_dot' => max( 0, min( 168, (int) ( $in['menu_dot'] ?? 0 ) ) ), 'subscribe_pos' => in_array( $in['subscribe_pos'] ?? '', array( 'top', 'bottom', 'both', 'none' ), true ) ? $in['subscribe_pos'] : 'bottom', 'share_pos' => in_array( $in['share_pos'] ?? '', array( 'top', 'bottom', 'both' ), true ) ? $in['share_pos'] : 'bottom', 'nvd_key' => sanitize_text_field( $in['nvd_key'] ?? '' ), 'page_id' => (int) ( $in['page_id'] ?? $old['page_id'] ), 'intro' => sanitize_textarea_field( $in['intro'] ?? '' ), 'cta_text' => sanitize_text_field( $in['cta_text'] ?? '' ), 'cta_url' => esc_url_raw( $in['cta_url'] ?? '' ), 'credit' => empty( $in['credit'] ) ? 0 : 1, 'accent' => sanitize_hex_color( $in['accent'] ?? '' ) ? sanitize_hex_color( $in['accent'] ) : '#2271b1', 'products' => sanitize_textarea_field( $in['products'] ?? '' ), ); if ( $out['page_id'] !== (int) $old['page_id'] ) { update_option( 'cpradar_flush', 1, false ); } return $out; } add_action( 'update_option_cpradar_settings', 'cpradar_after_save', 10, 2 ); add_action( 'add_option_cpradar_settings', 'cpradar_after_save' ); /** * Refetch after saving (and refresh URLs if the page changed). * * @param mixed $old_value Previous settings (only on update). * @param mixed $value New settings (only on update). */ function cpradar_after_save( $old_value = array(), $value = array() ) { if ( is_array( $old_value ) && is_array( $value ) && ( $old_value['nvd_key'] ?? '' ) !== ( $value['nvd_key'] ?? '' ) ) { delete_option( 'cpradar_nvd_key_error' ); } if ( get_option( 'cpradar_flush' ) ) { delete_option( 'cpradar_flush' ); cpradar_rewrite_rules(); flush_rewrite_rules( false ); } cpradar_schedule(); if ( is_admin() ) { cpradar_fetch(); // Does nothing while fetching is switched off. } } add_action( 'admin_post_cpradar_fetch_now', function () { check_admin_referer( 'cpradar_fetch_now' ); if ( current_user_can( 'manage_options' ) ) { cpradar_fetch(); } wp_safe_redirect( admin_url( 'options-general.php?page=syncnet-patch-radar' ) ); exit; } ); add_action( 'admin_post_cpradar_check', function () { check_admin_referer( 'cpradar_check' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to do that.', 'syncnet-patch-radar' ) ); } $cve = strtoupper( trim( sanitize_text_field( wp_unslash( $_POST['cve'] ?? '' ) ) ) ); set_transient( 'cpradar_check_' . get_current_user_id(), cpradar_check( $cve ), 10 * MINUTE_IN_SECONDS ); wp_safe_redirect( admin_url( 'options-general.php?page=syncnet-patch-radar#cpr-check' ) ); exit; } ); /** * Check one CVE against all conditions. * * @param string $cve CVE ID. * @return array */ function cpradar_check( $cve ) { $r = array( 'cve' => $cve, 'lines' => array(), ); $ok = function ( $text, $passed ) use ( &$r ) { $r['lines'][] = array( (bool) $passed, $text ); return $passed; }; if ( ! preg_match( '/^CVE-\d{4}-\d{4,}$/', $cve ) ) { $ok( __( 'Enter an ID in the format CVE-YYYY-NNNN', 'syncnet-patch-radar' ), false ); return $r; } $stored = cpradar_alert( $cve ); $ok( __( 'Currently listed', 'syncnet-patch-radar' ) . ( $stored ? ' (' . strtoupper( $stored['source'] ) . ')' : '' ), (bool) $stored ); $key = cpradar_nvd_key(); $body = cpradar_get( add_query_arg( 'cveId', $cve, CPRADAR_NVD_URL ), '' !== $key ? array( 'apiKey' => $key ) : array() ); if ( is_wp_error( $body ) ) { $ok( __( 'NVD reachable', 'syncnet-patch-radar' ) . ': ' . $body->get_error_message(), false ); return $r; } $data = json_decode( $body, true ); $c = $data['vulnerabilities'][0]['cve'] ?? null; if ( ! $c ) { $ok( __( 'Found at the NVD', 'syncnet-patch-radar' ), false ); return $r; } $s = cpradar_settings(); $limit = time() - DAY_IN_SECONDS * max( 7, (int) $s['days'] ); $date = strtotime( (string) ( $c['published'] ?? '' ) . 'Z' ); $cna = cpradar_nvd_cna( $c['sourceIdentifier'] ?? '' ); $best = cpradar_nvd_best_metric( $c ); $text = cpradar_nvd_description( $c ); /* translators: %s: status */ $ok( sprintf( __( 'Found at the NVD (status: %s)', 'syncnet-patch-radar' ), $c['vulnStatus'] ?? '?' ), true ); /* translators: 1: date, 2: number of days */ $ok( sprintf( __( 'Published %1$s – within %2$d days', 'syncnet-patch-radar' ), $date ? cpradar_date( $date ) : '?', (int) $s['days'] ), $date && $date >= $limit ); /* translators: 1: source identifier, 2: vendor or note */ $ok( sprintf( __( 'Reported by the vendor itself – source: "%1$s" %2$s', 'syncnet-patch-radar' ), $c['sourceIdentifier'] ?? '', $cna ? '= ' . $cna : __( '(not in the vendor list)', 'syncnet-patch-radar' ) ), '' !== $cna ); if ( $best ) { /* translators: 1: minimum, 2: actual score */ $ok( sprintf( __( 'CVSS at least %1$s (is %2$s)', 'syncnet-patch-radar' ), cpradar_score( $s['cvss'] ), cpradar_score( $best['baseScore'] ) ), (float) $best['baseScore'] >= (float) $s['cvss'] ); /* translators: %s: attack vector */ $ok( sprintf( __( 'Exploitable over the network (is %s)', 'syncnet-patch-radar' ), $best['attackVector'] ?? '?' ), 'NETWORK' === ( $best['attackVector'] ?? '' ) ); /* translators: %s: privileges required */ $ok( sprintf( __( 'No authentication needed (is %s)', 'syncnet-patch-radar' ), $best['privilegesRequired'] ?? '?' ), 'NONE' === ( $best['privilegesRequired'] ?? '' ) ); } else { $ok( __( 'CVSS 3 score available', 'syncnet-patch-radar' ), false ); } $product = $cna ? cpradar_nvd_product( $cna, $text ) : ''; /* translators: %s: product or "none" */ $ok( sprintf( __( 'Matching product: %s', 'syncnet-patch-radar' ), $product ? $product : __( 'none', 'syncnet-patch-radar' ) ), '' !== $product ); $r['text'] = cpradar_short( $text, 300 ); return $r; } /** * Settings page. */ function cpradar_admin_page() { $s = cpradar_settings(); $status = (array) get_option( 'cpradar_status', array() ); $progress = (array) get_option( 'cpradar_nvd_progress', array( 0, 0 ) ); $check = get_transient( 'cpradar_check_' . get_current_user_id() ); ?>

' . esc_html__( 'Matching in last fetch:', 'syncnet-patch-radar' ) . ' ' . esc_html( implode( ' · ', array_map( fn( $k, $v ) => $k . ' ' . $v, array_keys( $status['sources'] ), $status['sources'] ) ) ); } if ( get_option( 'cpradar_nvd_index' ) ) { /* translators: 1: checked, 2: total */ echo '
' . esc_html( sprintf( __( 'NVD: initial load running – %1$d of %2$d vulnerabilities checked. Continues hourly, or click "Fetch now" again.', 'syncnet-patch-radar' ), (int) $progress[0], (int) $progress[1] ) ) . ''; } $key_error = get_option( 'cpradar_nvd_key_error' ); if ( $key_error && '' !== trim( (string) $s['nvd_key'] ) ) { /* translators: 1: HTTP status code, 2: date and time */ echo '
' . esc_html__( 'NVD API key rejected', 'syncnet-patch-radar' ) . ' ' . esc_html( sprintf( __( '(HTTP status %1$d, %2$s). Fetching continues without the key, just more slowly. Please check: did you click the confirmation link in the e-mail from the NVD? Is the key entered exactly as in the e-mail?', 'syncnet-patch-radar' ), (int) $key_error[1], wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), (int) $key_error[0] ) ) ) . ''; } if ( ! empty( $status['errors'] ) ) { echo '
' . esc_html__( 'Errors:', 'syncnet-patch-radar' ) . ' ' . esc_html( implode( ' · ', $status['errors'] ) ) . ''; } ?>

' . esc_html__( 'Settings', 'syncnet-patch-radar' ) . '' ); return $links; } ); add_action( 'admin_notices', function () { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || 'plugins' !== $screen->id || ! current_user_can( 'manage_options' ) || ! empty( cpradar_settings()['enabled'] ) ) { return; } echo '

' . esc_html__( 'SyncNet Patch Radar is installed. Nothing is fetched until you switch fetching on:', 'syncnet-patch-radar' ) . ' ' . esc_html__( 'Open settings', 'syncnet-patch-radar' ) . '

'; } );