Coverage Report

Created: 2026-01-16 07:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/suricata7/rust/src/rdp/log.rs
Line
Count
Source
1
/* Copyright (C) 2019 Open Information Security Foundation
2
 *
3
 * You can copy, redistribute or modify this Program under the terms of
4
 * the GNU General Public License version 2 as published by the Free
5
 * Software Foundation.
6
 *
7
 * This program is distributed in the hope that it will be useful,
8
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10
 * GNU General Public License for more details.
11
 *
12
 * You should have received a copy of the GNU General Public License
13
 * version 2 along with this program; if not, write to the Free Software
14
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
15
 * 02110-1301, USA.
16
 */
17
18
// Author: Zach Kelly <zach.kelly@lmco.com>
19
20
use super::rdp::{RdpTransaction, RdpTransactionItem};
21
use crate::jsonbuilder::{JsonBuilder, JsonError};
22
use crate::rdp::parser::*;
23
use crate::rdp::windows;
24
use x509_parser::prelude::{X509Certificate, FromDer};
25
26
#[no_mangle]
27
164
pub extern "C" fn rs_rdp_to_json(tx: &mut RdpTransaction, js: &mut JsonBuilder) -> bool {
28
164
    log(tx, js).is_ok()
29
164
}
30
31
/// populate a json object with transactional information, for logging
32
164
fn log(tx: &RdpTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> {
33
164
    js.open_object("rdp")?;
34
164
    js.set_uint("tx_id", tx.id)?;
35
36
164
    match &tx.item {
37
75
        RdpTransactionItem::X224ConnectionRequest(ref x224) => x224_req_to_json(x224, js)?,
38
54
        RdpTransactionItem::X224ConnectionConfirm(ref x224) => x224_conf_to_json(x224, js)?,
39
40
24
        RdpTransactionItem::McsConnectRequest(ref mcs) => {
41
24
            mcs_req_to_json(mcs, js)?;
42
        }
43
44
        RdpTransactionItem::McsConnectResponse(_) => {
45
            // no additional JSON data beyond `event_type`
46
10
            js.set_string("event_type", "connect_response")?;
47
        }
48
49
1
        RdpTransactionItem::TlsCertificateChain(chain) => {
50
1
            js.set_string("event_type", "tls_handshake")?;
51
1
            js.open_array("x509_serials")?;
52
31
            for blob in chain {
53
30
                if let Ok((_, cert)) = X509Certificate::from_der(&blob.data) {
54
0
                    js.append_string(&cert.tbs_certificate.serial.to_str_radix(16))?;
55
30
                }
56
            }
57
1
            js.close()?;
58
        }
59
    }
60
61
164
    js.close()?;
62
164
    Ok(())
63
164
}
64
65
/// json helper for X224ConnectionRequest
66
75
fn x224_req_to_json(x224: &X224ConnectionRequest, js: &mut JsonBuilder) -> Result<(), JsonError> {
67
    use crate::rdp::parser::NegotiationRequestFlags as Flags;
68
69
75
    js.set_string("event_type", "initial_request")?;
70
75
    if let Some(ref cookie) = x224.cookie {
71
60
        js.set_string("cookie", &cookie.mstshash)?;
72
15
    }
73
75
    if let Some(ref req) = x224.negotiation_request {
74
0
        if !req.flags.is_empty() {
75
0
            js.open_array("flags")?;
76
0
            if req.flags.contains(Flags::RESTRICTED_ADMIN_MODE_REQUIRED) {
77
0
                js.append_string("restricted_admin_mode_required")?;
78
0
            }
79
0
            if req
80
0
                .flags
81
0
                .contains(Flags::REDIRECTED_AUTHENTICATION_MODE_REQUIRED)
82
            {
83
0
                js.append_string("redirected_authentication_mode_required")?;
84
0
            }
85
0
            if req.flags.contains(Flags::CORRELATION_INFO_PRESENT) {
86
0
                js.append_string("correlation_info_present")?;
87
0
            }
88
0
            js.close()?;
89
0
        }
90
75
    }
91
92
75
    Ok(())
93
75
}
94
95
/// json helper for X224ConnectionConfirm
96
54
fn x224_conf_to_json(x224: &X224ConnectionConfirm, js: &mut JsonBuilder) -> Result<(), JsonError> {
97
    use crate::rdp::parser::NegotiationResponseFlags as Flags;
98
99
54
    js.set_string("event_type", "initial_response")?;
100
54
    if let Some(ref from_server) = x224.negotiation_from_server {
101
0
        match &from_server {
102
0
            NegotiationFromServer::Response(ref resp) => {
103
0
                if !resp.flags.is_empty() {
104
0
                    js.open_array("server_supports")?;
105
0
                    if resp.flags.contains(Flags::EXTENDED_CLIENT_DATA_SUPPORTED) {
106
0
                        js.append_string("extended_client_data")?;
107
0
                    }
108
0
                    if resp.flags.contains(Flags::DYNVC_GFX_PROTOCOL_SUPPORTED) {
109
0
                        js.append_string("dynvc_gfx")?;
110
0
                    }
111
112
                    // NEGRSP_FLAG_RESERVED not logged
113
114
0
                    if resp.flags.contains(Flags::RESTRICTED_ADMIN_MODE_SUPPORTED) {
115
0
                        js.append_string("restricted_admin")?;
116
0
                    }
117
0
                    if resp
118
0
                        .flags
119
0
                        .contains(Flags::REDIRECTED_AUTHENTICATION_MODE_SUPPORTED)
120
                    {
121
0
                        js.append_string("redirected_authentication")?;
122
0
                    }
123
0
                    js.close()?;
124
0
                }
125
126
0
                let protocol = match resp.protocol {
127
0
                    Protocol::ProtocolRdp => "rdp",
128
0
                    Protocol::ProtocolSsl => "ssl",
129
0
                    Protocol::ProtocolHybrid => "hybrid",
130
0
                    Protocol::ProtocolRdsTls => "rds_tls",
131
0
                    Protocol::ProtocolHybridEx => "hybrid_ex",
132
                };
133
0
                js.set_string("protocol", protocol)?;
134
            }
135
136
0
            NegotiationFromServer::Failure(ref fail) => match fail.code {
137
                NegotiationFailureCode::SslRequiredByServer => {
138
0
                    js.set_uint(
139
0
                        "error_code",
140
0
                        NegotiationFailureCode::SslRequiredByServer as u64,
141
0
                    )?;
142
0
                    js.set_string("reason", "ssl required by server")?;
143
                }
144
                NegotiationFailureCode::SslNotAllowedByServer => {
145
0
                    js.set_uint(
146
0
                        "error_code",
147
0
                        NegotiationFailureCode::SslNotAllowedByServer as u64,
148
0
                    )?;
149
0
                    js.set_string("reason", "ssl not allowed by server")?;
150
                }
151
                NegotiationFailureCode::SslCertNotOnServer => {
152
0
                    js.set_uint(
153
0
                        "error_code",
154
0
                        NegotiationFailureCode::SslCertNotOnServer as u64,
155
0
                    )?;
156
0
                    js.set_string("reason", "ssl cert not on server")?;
157
                }
158
                NegotiationFailureCode::InconsistentFlags => {
159
0
                    js.set_uint(
160
0
                        "error_code",
161
0
                        NegotiationFailureCode::InconsistentFlags as u64,
162
0
                    )?;
163
0
                    js.set_string("reason", "inconsistent flags")?;
164
                }
165
                NegotiationFailureCode::HybridRequiredByServer => {
166
0
                    js.set_uint(
167
0
                        "error_code",
168
0
                        NegotiationFailureCode::HybridRequiredByServer as u64,
169
0
                    )?;
170
0
                    js.set_string("reason", "hybrid required by server")?;
171
                }
172
                NegotiationFailureCode::SslWithUserAuthRequiredByServer => {
173
0
                    js.set_uint(
174
0
                        "error_code",
175
0
                        NegotiationFailureCode::SslWithUserAuthRequiredByServer as u64,
176
0
                    )?;
177
0
                    js.set_string("reason", "ssl with user auth required by server")?;
178
                }
179
            },
180
        }
181
54
    }
182
183
54
    Ok(())
184
54
}
185
186
/// json helper for McsConnectRequest
187
24
fn mcs_req_to_json(mcs: &McsConnectRequest, js: &mut JsonBuilder) -> Result<(), JsonError> {
188
    // placeholder string value.  We do not simply omit "unknown" values so that they can
189
    // help indicate that a given enum may be out of date (new Windows version, etc.)
190
24
    let unknown = String::from("unknown");
191
192
24
    js.set_string("event_type", "connect_request")?;
193
83
    for child in &mcs.children {
194
59
        match child {
195
24
            McsConnectRequestChild::CsClientCore(ref client) => {
196
24
                js.open_object("client")?;
197
198
24
                match client.version {
199
24
                    Some(ref ver) => {
200
24
                        js.set_string("version", &version_to_string(ver, "v"))?;
201
                    }
202
                    None => {
203
0
                        js.set_string("version", &unknown)?;
204
                    }
205
                }
206
207
24
                js.set_uint("desktop_width", client.desktop_width as u64)?;
208
24
                js.set_uint("desktop_height", client.desktop_height as u64)?;
209
210
24
                if let Some(depth) = get_color_depth(client) {
211
20
                    js.set_uint("color_depth", depth)?;
212
4
                }
213
214
                // sas_sequence not logged
215
216
24
                js.set_string(
217
24
                    "keyboard_layout",
218
24
                    &windows::lcid_to_string(client.keyboard_layout, &unknown),
219
0
                )?;
220
221
24
                js.set_string(
222
24
                    "build",
223
24
                    &windows::os_to_string(&client.client_build, &unknown),
224
0
                )?;
225
226
24
                if !client.client_name.is_empty() {
227
24
                    js.set_string("client_name", &client.client_name)?;
228
0
                }
229
230
24
                if let Some(ref kb) = client.keyboard_type {
231
14
                    js.set_string("keyboard_type", &keyboard_to_string(kb))?;
232
10
                }
233
234
24
                if client.keyboard_subtype != 0 {
235
8
                    js.set_uint("keyboard_subtype", client.keyboard_subtype as u64)?;
236
16
                }
237
238
24
                if client.keyboard_function_key != 0 {
239
23
                    js.set_uint("function_keys", client.keyboard_function_key as u64)?;
240
1
                }
241
242
24
                if !client.ime_file_name.is_empty() {
243
8
                    js.set_string("ime", &client.ime_file_name)?;
244
16
                }
245
246
                //
247
                // optional fields
248
                //
249
250
24
                if let Some(id) = client.client_product_id {
251
21
                    js.set_uint("product_id", id as u64)?;
252
3
                }
253
254
24
                if let Some(serial) = client.serial_number {
255
21
                    if serial != 0 {
256
3
                        js.set_uint("serial_number", serial as u64)?;
257
18
                    }
258
3
                }
259
260
                // supported_color_depth not logged
261
262
24
                if let Some(ref early_capability_flags) = client.early_capability_flags {
263
                    use crate::rdp::parser::EarlyCapabilityFlags as Flags;
264
265
18
                    if !early_capability_flags.is_empty() {
266
16
                        js.open_array("capabilities")?;
267
16
                        if early_capability_flags.contains(Flags::RNS_UD_CS_SUPPORT_ERRINFO_PDF) {
268
16
                            js.append_string("support_errinfo_pdf")?;
269
0
                        }
270
16
                        if early_capability_flags.contains(Flags::RNS_UD_CS_WANT_32BPP_SESSION) {
271
2
                            js.append_string("want_32bpp_session")?;
272
14
                        }
273
16
                        if early_capability_flags.contains(Flags::RNS_UD_CS_SUPPORT_STATUSINFO_PDU)
274
                        {
275
2
                            js.append_string("support_statusinfo_pdu")?;
276
14
                        }
277
16
                        if early_capability_flags.contains(Flags::RNS_UD_CS_STRONG_ASYMMETRIC_KEYS)
278
                        {
279
0
                            js.append_string("strong_asymmetric_keys")?;
280
16
                        }
281
282
                        // RNS_UD_CS_UNUSED not logged
283
284
16
                        if early_capability_flags.contains(Flags::RNS_UD_CS_VALID_CONNECTION_TYPE) {
285
0
                            js.append_string("valid_connection_type")?;
286
16
                        }
287
16
                        if early_capability_flags
288
16
                            .contains(Flags::RNS_UD_CS_SUPPORT_MONITOR_LAYOUT_PDU)
289
                        {
290
0
                            js.append_string("support_monitor_layout_pdu")?;
291
16
                        }
292
16
                        if early_capability_flags
293
16
                            .contains(Flags::RNS_UD_CS_SUPPORT_NETCHAR_AUTODETECT)
294
                        {
295
0
                            js.append_string("support_netchar_autodetect")?;
296
16
                        }
297
16
                        if early_capability_flags
298
16
                            .contains(Flags::RNS_UD_CS_SUPPORT_DYNVC_GFX_PROTOCOL)
299
                        {
300
0
                            js.append_string("support_dynvc_gfx_protocol")?;
301
16
                        }
302
16
                        if early_capability_flags
303
16
                            .contains(Flags::RNS_UD_CS_SUPPORT_DYNAMIC_TIME_ZONE)
304
                        {
305
0
                            js.append_string("support_dynamic_time_zone")?;
306
16
                        }
307
16
                        if early_capability_flags.contains(Flags::RNS_UD_CS_SUPPORT_HEARTBEAT_PDU) {
308
0
                            js.append_string("support_heartbeat_pdu")?;
309
16
                        }
310
16
                        js.close()?;
311
2
                    }
312
6
                }
313
314
24
                if let Some(ref id) = client.client_dig_product_id {
315
16
                    if !id.is_empty() {
316
14
                        js.set_string("id", id)?;
317
2
                    }
318
8
                }
319
320
24
                if let Some(ref hint) = client.connection_hint {
321
16
                    let s = match hint {
322
1
                        ConnectionHint::ConnectionHintModem => "modem",
323
1
                        ConnectionHint::ConnectionHintBroadbandLow => "low_broadband",
324
0
                        ConnectionHint::ConnectionHintSatellite => "satellite",
325
0
                        ConnectionHint::ConnectionHintBroadbandHigh => "high_broadband",
326
0
                        ConnectionHint::ConnectionHintWan => "wan",
327
0
                        ConnectionHint::ConnectionHintLan => "lan",
328
0
                        ConnectionHint::ConnectionHintAutoDetect => "autodetect",
329
14
                        ConnectionHint::ConnectionHintNotProvided => "",
330
                    };
331
16
                    if *hint != ConnectionHint::ConnectionHintNotProvided {
332
2
                        js.set_string("connection_hint", s)?;
333
14
                    }
334
8
                }
335
336
                // server_selected_protocol not logged
337
338
24
                if let Some(width) = client.desktop_physical_width {
339
0
                    js.set_uint("physical_width", width as u64)?;
340
24
                }
341
342
24
                if let Some(height) = client.desktop_physical_height {
343
0
                    js.set_uint("physical_height", height as u64)?;
344
24
                }
345
346
24
                if let Some(orientation) = client.desktop_orientation {
347
0
                    js.set_uint("desktop_orientation", orientation as u64)?;
348
24
                }
349
350
24
                if let Some(scale) = client.desktop_scale_factor {
351
0
                    js.set_uint("scale_factor", scale as u64)?;
352
24
                }
353
354
24
                if let Some(scale) = client.device_scale_factor {
355
0
                    js.set_uint("device_scale_factor", scale as u64)?;
356
24
                }
357
24
                js.close()?;
358
            }
359
360
11
            McsConnectRequestChild::CsNet(ref net) => {
361
11
                if !net.channels.is_empty() {
362
11
                    js.open_array("channels")?;
363
44
                    for channel in &net.channels {
364
33
                        js.append_string(channel)?;
365
                    }
366
11
                    js.close()?;
367
0
                }
368
            }
369
370
24
            McsConnectRequestChild::CsUnknown(_) => {}
371
        }
372
    }
373
374
24
    Ok(())
375
24
}
376
377
/// converts RdpClientVersion to a string, using the provided prefix
378
24
fn version_to_string(ver: &RdpClientVersion, prefix: &str) -> String {
379
24
    let mut result = String::from(prefix);
380
24
    match ver {
381
0
        RdpClientVersion::V4 => result.push('4'),
382
24
        RdpClientVersion::V5_V8_1 => result.push('5'),
383
0
        RdpClientVersion::V10_0 => result.push_str("10.0"),
384
0
        RdpClientVersion::V10_1 => result.push_str("10.1"),
385
0
        RdpClientVersion::V10_2 => result.push_str("10.2"),
386
0
        RdpClientVersion::V10_3 => result.push_str("10.3"),
387
0
        RdpClientVersion::V10_4 => result.push_str("10.4"),
388
0
        RdpClientVersion::V10_5 => result.push_str("10.5"),
389
0
        RdpClientVersion::V10_6 => result.push_str("10.6"),
390
0
        RdpClientVersion::V10_7 => result.push_str("10.7"),
391
    };
392
24
    result
393
24
}
394
395
/// checks multiple client info fields to determine color depth
396
24
fn get_color_depth(client: &CsClientCoreData) -> Option<u64> {
397
    // first check high_color_depth
398
19
    match client.high_color_depth {
399
1
        Some(HighColorDepth::HighColor4Bpp) => return Some(4),
400
0
        Some(HighColorDepth::HighColor8Bpp) => return Some(8),
401
14
        Some(HighColorDepth::HighColor15Bpp) => return Some(15),
402
0
        Some(HighColorDepth::HighColor16Bpp) => return Some(16),
403
0
        Some(HighColorDepth::HighColor24Bpp) => return Some(24),
404
9
        _ => (),
405
    };
406
407
    // if not present, try post_beta2_color_depth
408
6
    match client.post_beta2_color_depth {
409
0
        Some(PostBeta2ColorDepth::RnsUdColor4Bpp) => return Some(4),
410
0
        Some(PostBeta2ColorDepth::RnsUdColor8Bpp) => return Some(8),
411
0
        Some(PostBeta2ColorDepth::RnsUdColor16Bpp555) => return Some(15),
412
0
        Some(PostBeta2ColorDepth::RnsUdColor16Bpp565) => return Some(16),
413
0
        Some(PostBeta2ColorDepth::RnsUdColor24Bpp) => return Some(24),
414
9
        _ => (),
415
    };
416
417
    // if not present, try color_depth
418
5
    match client.color_depth {
419
0
        Some(ColorDepth::RnsUdColor4Bpp) => return Some(4),
420
5
        Some(ColorDepth::RnsUdColor8Bpp) => return Some(8),
421
4
        _ => return None,
422
    }
423
24
}
424
425
14
fn keyboard_to_string(kb: &KeyboardType) -> String {
426
14
    let s = match kb {
427
0
        KeyboardType::KbXt => "xt",
428
0
        KeyboardType::KbIco => "ico",
429
0
        KeyboardType::KbAt => "at",
430
14
        KeyboardType::KbEnhanced => "enhanced",
431
0
        KeyboardType::Kb1050 => "1050",
432
0
        KeyboardType::Kb9140 => "9140",
433
0
        KeyboardType::KbJapanese => "jp",
434
    };
435
14
    String::from(s)
436
14
}
437
438
#[cfg(test)]
439
mod tests {
440
    use super::*;
441
442
    // for now, testing of JsonBuilder output is done by suricata-verify
443
444
    #[test]
445
    fn test_version_string() {
446
        assert_eq!("v10.7", version_to_string(&RdpClientVersion::V10_7, "v"));
447
    }
448
449
    #[test]
450
    fn test_color_depth_high() {
451
        let core_data = CsClientCoreData {
452
            version: None,
453
            desktop_width: 1280,
454
            desktop_height: 768,
455
            color_depth: Some(ColorDepth::RnsUdColor4Bpp),
456
            sas_sequence: None,
457
            keyboard_layout: 0x409,
458
            client_build: windows::OperatingSystem {
459
                build: windows::Build::Win10_17763,
460
                suffix: windows::Suffix::Rs5,
461
            },
462
            client_name: String::from("SERVER-XYZ"),
463
            keyboard_type: None,
464
            keyboard_subtype: 0,
465
            keyboard_function_key: 12,
466
            ime_file_name: String::from(""),
467
            post_beta2_color_depth: Some(PostBeta2ColorDepth::RnsUdColor8Bpp),
468
            client_product_id: None,
469
            serial_number: None,
470
            high_color_depth: Some(HighColorDepth::HighColor24Bpp),
471
            supported_color_depth: None,
472
            early_capability_flags: None,
473
            client_dig_product_id: None,
474
            connection_hint: None,
475
            server_selected_protocol: None,
476
            desktop_physical_width: None,
477
            desktop_physical_height: None,
478
            desktop_orientation: None,
479
            desktop_scale_factor: None,
480
            device_scale_factor: None,
481
        };
482
        assert_eq!(Some(24), get_color_depth(&core_data));
483
    }
484
485
    #[test]
486
    fn test_color_depth_post_beta2() {
487
        let core_data = CsClientCoreData {
488
            version: None,
489
            desktop_width: 1280,
490
            desktop_height: 768,
491
            color_depth: Some(ColorDepth::RnsUdColor4Bpp),
492
            sas_sequence: None,
493
            keyboard_layout: 0x409,
494
            client_build: windows::OperatingSystem {
495
                build: windows::Build::Win10_17763,
496
                suffix: windows::Suffix::Rs5,
497
            },
498
            client_name: String::from("SERVER-XYZ"),
499
            keyboard_type: None,
500
            keyboard_subtype: 0,
501
            keyboard_function_key: 12,
502
            ime_file_name: String::from(""),
503
            post_beta2_color_depth: Some(PostBeta2ColorDepth::RnsUdColor8Bpp),
504
            client_product_id: None,
505
            serial_number: None,
506
            high_color_depth: None,
507
            supported_color_depth: None,
508
            early_capability_flags: None,
509
            client_dig_product_id: None,
510
            connection_hint: None,
511
            server_selected_protocol: None,
512
            desktop_physical_width: None,
513
            desktop_physical_height: None,
514
            desktop_orientation: None,
515
            desktop_scale_factor: None,
516
            device_scale_factor: None,
517
        };
518
        assert_eq!(Some(8), get_color_depth(&core_data));
519
    }
520
521
    #[test]
522
    fn test_color_depth_basic() {
523
        let core_data = CsClientCoreData {
524
            version: None,
525
            desktop_width: 1280,
526
            desktop_height: 768,
527
            color_depth: Some(ColorDepth::RnsUdColor4Bpp),
528
            sas_sequence: None,
529
            keyboard_layout: 0x409,
530
            client_build: windows::OperatingSystem {
531
                build: windows::Build::Win10_17763,
532
                suffix: windows::Suffix::Rs5,
533
            },
534
            client_name: String::from("SERVER-XYZ"),
535
            keyboard_type: None,
536
            keyboard_subtype: 0,
537
            keyboard_function_key: 12,
538
            ime_file_name: String::from(""),
539
            post_beta2_color_depth: None,
540
            client_product_id: None,
541
            serial_number: None,
542
            high_color_depth: None,
543
            supported_color_depth: None,
544
            early_capability_flags: None,
545
            client_dig_product_id: None,
546
            connection_hint: None,
547
            server_selected_protocol: None,
548
            desktop_physical_width: None,
549
            desktop_physical_height: None,
550
            desktop_orientation: None,
551
            desktop_scale_factor: None,
552
            device_scale_factor: None,
553
        };
554
        assert_eq!(Some(4), get_color_depth(&core_data));
555
    }
556
557
    #[test]
558
    fn test_color_depth_missing() {
559
        let core_data = CsClientCoreData {
560
            version: None,
561
            desktop_width: 1280,
562
            desktop_height: 768,
563
            color_depth: None,
564
            sas_sequence: None,
565
            keyboard_layout: 0x409,
566
            client_build: windows::OperatingSystem {
567
                build: windows::Build::Win10_17763,
568
                suffix: windows::Suffix::Rs5,
569
            },
570
            client_name: String::from("SERVER-XYZ"),
571
            keyboard_type: None,
572
            keyboard_subtype: 0,
573
            keyboard_function_key: 12,
574
            ime_file_name: String::from(""),
575
            post_beta2_color_depth: None,
576
            client_product_id: None,
577
            serial_number: None,
578
            high_color_depth: None,
579
            supported_color_depth: None,
580
            early_capability_flags: None,
581
            client_dig_product_id: None,
582
            connection_hint: None,
583
            server_selected_protocol: None,
584
            desktop_physical_width: None,
585
            desktop_physical_height: None,
586
            desktop_orientation: None,
587
            desktop_scale_factor: None,
588
            device_scale_factor: None,
589
        };
590
        assert!(get_color_depth(&core_data).is_none());
591
    }
592
593
    #[test]
594
    fn test_keyboard_string() {
595
        assert_eq!("enhanced", keyboard_to_string(&KeyboardType::KbEnhanced));
596
    }
597
}